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:
Monica Gupta
2020-08-12 15:34:38 -07:00
committed by GitHub
parent d2f5bfaa16
commit 148b6e398d
276 changed files with 75983 additions and 1 deletions

View 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
}
}

View File

@@ -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}'";
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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");
}
}

View File

@@ -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");
}
}

View File

@@ -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");
}
}

View File

@@ -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; }
}
}

View File

@@ -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
{
}
}

View File

@@ -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");
}
}

View File

@@ -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");
}
}

View File

@@ -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");
}
}

View File

@@ -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);
}
}

View File

@@ -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");
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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");
}
}

View File

@@ -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");
}
}

View File

@@ -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");
}
}

View File

@@ -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}'";
}
}

View File

@@ -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; }
}
}

View File

@@ -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}'";
}
}

View File

@@ -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");
}
}

View File

@@ -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
};
}
}
}

View File

@@ -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";
}
}

View File

@@ -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");
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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
}
}

View File

@@ -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 &lt;255, 5 if the length is &gt;=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
}
}

View File

@@ -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);
}
}
}
}
}

View 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
}
}

View File

@@ -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

View File

@@ -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));
}
}
}

View File

@@ -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);
}
}
}

View 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
}
}

View File

@@ -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
};
}
}
}

View File

@@ -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
};
}