mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-01-19 17:23:55 -05:00
port batch parser wrapper (#232)
* Initial commit for GitHub IO pages * Add initial doxfx content * Update manifest.json * Update manifest.json * Set theme jekyll-theme-cayman * Set theme jekyll-theme-slate * Set theme jekyll-theme-minimal * Set theme jekyll-theme-tactile * Clear out theme setting * Remove API docs * Revert "Adding Milliseconds to DateTime fields (#173)" (#197) This reverts commit431dfa4156. * ported new BatchParser * added BatchParser tests * fixing merge conflicts * fix build issues * cleaned code and addressed comments from code review * addressed code review and made BatchParser logic more efficient * fixed batch parser tests * changed class name to fix build issues * fixed merge conflicts * added path for lab mode baseline tests * changed env path for lab mode * added env variable to appveyor * testing env variable for appveyor * fixed lab build * debug appveyor build * testing changes for appveyor * changed trace env path * debugging appveyor build * changed baseline env path * debugging * debugging * debugging * switched on trace flag * debugging * debugging * changed build config * changed baseline files * checking baseline output * changed baseline files * debug baseline tests * debugging baseline * debugging * debugging * debug * debugging * testing baseline format * debug * debug * debug * debug * debug * newline debug * changed baseline file * debug * test * try new way to read * added execution engine tests * change test * testing file encoding * moved execution engine tests to integration * try compare without spaces * removed no op test * added env variable for travis * put batch parser tests to integration too * put batch parser in integration * try new baseline string match * compare baseline test logic changed * changed baseline logic as well as cleaned tests * fix build for travis CI * fix travis CI issues * fixed highlighting bugs on vscode * code review changes * ported new BatchParser * added BatchParser tests * Initial commit for GitHub IO pages * Add initial doxfx content * Update manifest.json * Update manifest.json * Set theme jekyll-theme-cayman * Set theme jekyll-theme-slate * Set theme jekyll-theme-minimal * Set theme jekyll-theme-tactile * Clear out theme setting * Remove API docs * Revert "Adding Milliseconds to DateTime fields (#173)" (#197) This reverts commit431dfa4156. * fixing merge conflicts * fix build issues * cleaned code and addressed comments from code review * addressed code review and made BatchParser logic more efficient * fixed batch parser tests * changed class name to fix build issues * fixed merge conflicts * added path for lab mode baseline tests changed env path for lab mode added env variable to appveyor testing env variable for appveyor fixed lab build debug appveyor build testing changes for appveyor changed trace env path debugging appveyor build changed baseline env path debugging debugging debugging switched on trace flag debugging debugging changed build config changed baseline files checking baseline output changed baseline files debug baseline tests debugging baseline debugging debugging debug debugging testing baseline format debug debug debug debug debug newline debug changed baseline file debug test try new way to read added execution engine tests change test testing file encoding moved execution engine tests to integration try compare without spaces removed no op test added env variable for travis * put batch parser tests to integration too * put batch parser in integration try new baseline string match * compare baseline test logic changed * changed baseline logic as well as cleaned tests * fix build for travis CI * fix travis CI issues * fixed highlighting bugs on vscode * code review changes * fixed filestream writer test * added localization string * added localization string * generated new string files again * code review changes
This commit is contained in:
@@ -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.SqlTools.ServiceLayer.BatchParser
|
||||
{
|
||||
internal enum BatchParserAction
|
||||
{
|
||||
Continue = 0,
|
||||
Abort = 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// 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.SqlTools.ServiceLayer.BatchParser
|
||||
{
|
||||
internal sealed class BatchParserException : Exception
|
||||
{
|
||||
const string ErrorCodeName = "ErrorCode";
|
||||
const string BeginName = "Begin";
|
||||
const string EndName = "End";
|
||||
const string TextName = "Text";
|
||||
const string TokenTypeName = "TokenType";
|
||||
|
||||
ErrorCode errorCode;
|
||||
PositionStruct begin;
|
||||
PositionStruct end;
|
||||
string text;
|
||||
LexerTokenType tokenType;
|
||||
|
||||
/// <summary>
|
||||
/// Class for a custom exception for the Batch Parser
|
||||
/// </summary>
|
||||
public BatchParserException(ErrorCode errorCode, Token token, string message)
|
||||
: base(message)
|
||||
{
|
||||
this.errorCode = errorCode;
|
||||
begin = token.Begin;
|
||||
end = token.End;
|
||||
text = token.Text;
|
||||
tokenType = token.TokenType;
|
||||
}
|
||||
|
||||
public ErrorCode ErrorCode { get { return errorCode; } }
|
||||
|
||||
public PositionStruct Begin { get { return begin; } }
|
||||
|
||||
public PositionStruct End { get { return end; } }
|
||||
|
||||
public string Text { get { return text; } }
|
||||
|
||||
public LexerTokenType TokenType { get { return tokenType; } }
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
//
|
||||
// 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.ServiceLayer.Utility;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.BatchParser
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Wraps the SMO Batch parser to make it a easily useable component.
|
||||
/// </summary>
|
||||
public sealed class BatchParserWrapper : IDisposable
|
||||
{
|
||||
|
||||
private List<Tuple<int /*startLine*/, int/*startColumn*/>> startLineColumns;
|
||||
private List<int /*length*/> lengths;
|
||||
private ExecutionEngine executionEngine;
|
||||
private BatchEventNotificationHandler notificationHandler;
|
||||
|
||||
/// <summary>
|
||||
/// Helper method used to Convert line/column information in a file to offset
|
||||
/// </summary>
|
||||
private static List<BatchDefinition> ConvertToBatchDefinitionList(string content,
|
||||
IList<Tuple<int, int>> positions, List<int> lengths)
|
||||
{
|
||||
|
||||
List<BatchDefinition> batchDefinitionList = new List<BatchDefinition>();
|
||||
|
||||
if (!string.IsNullOrEmpty(content) && (positions.Count > 0))
|
||||
{
|
||||
// Instantiate a string reader for the whole sql content
|
||||
using (StringReader reader = new StringReader(content))
|
||||
{
|
||||
|
||||
// Generate the first batch definition list
|
||||
int startLine = positions[0].Item1 + 1;
|
||||
int endLine = startLine;
|
||||
int lineDifference = 0;
|
||||
int endColumn;
|
||||
int lineStartOffset = 0;
|
||||
int offset = 0;
|
||||
int startColumn = positions[0].Item2;
|
||||
int count = positions.Count;
|
||||
string batchText = content.Substring(offset, lengths[0]);
|
||||
|
||||
// if there's only one batch then the line difference is just 0
|
||||
if (count > 1)
|
||||
{
|
||||
lineDifference = positions[1].Item1 - positions[0].Item1;
|
||||
}
|
||||
|
||||
// get endLine, endColumn for the current batch and the lineStartOffset for the next batch
|
||||
Tuple<int, int, int> batchInfo = ReadLines(reader, lineStartOffset, lineDifference, endLine);
|
||||
endLine = batchInfo.Item1;
|
||||
endColumn = batchInfo.Item2;
|
||||
lineStartOffset = batchInfo.Item3;
|
||||
|
||||
offset = lineStartOffset + positions[0].Item2;
|
||||
|
||||
// create a new BatchDefinition and add it to the list
|
||||
BatchDefinition batchDef = new BatchDefinition(
|
||||
batchText,
|
||||
startLine,
|
||||
endLine,
|
||||
startColumn + 1,
|
||||
endColumn
|
||||
);
|
||||
|
||||
batchDefinitionList.Add(batchDef);
|
||||
|
||||
// Generate the rest batch definitions
|
||||
for (int index = 1; index < count - 1; index++)
|
||||
{
|
||||
lineDifference = positions[index + 1].Item1 - positions[index].Item1;
|
||||
batchInfo = ReadLines(reader, lineStartOffset, lineDifference, endLine);
|
||||
endLine = batchInfo.Item1;
|
||||
endColumn = batchInfo.Item2;
|
||||
lineStartOffset = batchInfo.Item3;
|
||||
batchText = content.Substring(offset, lengths[index]);
|
||||
offset = lineStartOffset + positions[index].Item2;
|
||||
startLine = positions[index].Item1;
|
||||
startColumn = positions[index].Item2;
|
||||
|
||||
// make a new batch definition for each batch
|
||||
BatchDefinition batch = new BatchDefinition(
|
||||
batchText,
|
||||
startLine,
|
||||
endLine,
|
||||
startColumn + 1,
|
||||
endColumn
|
||||
);
|
||||
batchDefinitionList.Add(batch);
|
||||
}
|
||||
|
||||
// if there is only one batch then that was the last one anyway
|
||||
if (count > 1)
|
||||
{
|
||||
batchText = content.Substring(offset, lengths[count - 1]);
|
||||
BatchDefinition lastBatchDef = GetLastBatchDefinition(reader, positions[count - 1], batchText);
|
||||
batchDefinitionList.Add(lastBatchDef);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return batchDefinitionList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to get the last batch
|
||||
/// </summary>
|
||||
private static BatchDefinition GetLastBatchDefinition(StringReader reader,
|
||||
Tuple<int, int> position, string batchText)
|
||||
{
|
||||
int startLine = position.Item1;
|
||||
int startColumn = position.Item2;
|
||||
string prevLine = null;
|
||||
string line = reader.ReadLine();
|
||||
int endLine = startLine;
|
||||
|
||||
// find end line
|
||||
while (line != null)
|
||||
{
|
||||
endLine++;
|
||||
if (line != "\n")
|
||||
{
|
||||
prevLine = line;
|
||||
}
|
||||
line = reader.ReadLine();
|
||||
}
|
||||
|
||||
// get number of characters in the last line
|
||||
int endColumn = prevLine.ToCharArray().Length;
|
||||
|
||||
return new BatchDefinition(
|
||||
batchText,
|
||||
startLine,
|
||||
endLine,
|
||||
startColumn + 1,
|
||||
endColumn
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper function to get correct lines and columns
|
||||
/// in a single batch with multiple statements
|
||||
/// </summary>
|
||||
private static Tuple<int, int, int> GetBatchDetails(StringReader reader, int endLine)
|
||||
{
|
||||
string prevLine = null;
|
||||
string line = reader.ReadLine();
|
||||
|
||||
// find end line
|
||||
while (line != null)
|
||||
{
|
||||
endLine++;
|
||||
if (line != "\n")
|
||||
{
|
||||
prevLine = line;
|
||||
}
|
||||
line = reader.ReadLine();
|
||||
}
|
||||
|
||||
// get number of characters in the last line
|
||||
int endColumn = prevLine.ToCharArray().Length;
|
||||
|
||||
//lineOffset doesn't matter because its the last batch
|
||||
return Tuple.Create(endLine, endColumn, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read number of lines and get the line offset
|
||||
/// </summary>
|
||||
private static Tuple<int, int, int> ReadLines(StringReader reader, int lineStartOffset, int n, int endLine)
|
||||
{
|
||||
Validate.IsNotNull(nameof(reader), reader);
|
||||
int endColumn = 0;
|
||||
|
||||
// if only one batch with multiple lines
|
||||
if (n == 0)
|
||||
{
|
||||
return GetBatchDetails(reader, endLine);
|
||||
}
|
||||
|
||||
// if there are more than one batch
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
endColumn = 0;
|
||||
int ch;
|
||||
while (true)
|
||||
{
|
||||
ch = reader.Read();
|
||||
if (ch == -1) // EOF do nothing
|
||||
{
|
||||
break;
|
||||
}
|
||||
else if (ch == 10 /* for \n */) // End of line increase and break
|
||||
{
|
||||
++lineStartOffset;
|
||||
++endLine;
|
||||
break;
|
||||
}
|
||||
else // regular char just increase
|
||||
{
|
||||
++endColumn;
|
||||
++lineStartOffset;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return Tuple.Create(endLine, endColumn, lineStartOffset);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper API for the Batch Parser that returns a list of
|
||||
/// BatchDefinitions when given a string to parse
|
||||
/// </summary>
|
||||
public BatchParserWrapper()
|
||||
{
|
||||
executionEngine = new ExecutionEngine();
|
||||
|
||||
// subscribe to executionEngine BatchParser events
|
||||
executionEngine.BatchParserExecutionError += OnBatchParserExecutionError;
|
||||
|
||||
executionEngine.BatchParserExecutionFinished += OnBatchParserExecutionFinished;
|
||||
|
||||
// instantiate notificationHandler class
|
||||
notificationHandler = new BatchEventNotificationHandler();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes in a query string and returns a list of BatchDefinitions
|
||||
/// </summary>
|
||||
public List<BatchDefinition> GetBatches(string sqlScript)
|
||||
{
|
||||
startLineColumns = new List<System.Tuple<int /*startLine*/, int /*startColumn*/>>();
|
||||
lengths = new List<int /* length */>();
|
||||
|
||||
// execute the script - all communication / integration after here happen via event handlers
|
||||
executionEngine.ParseScript(sqlScript, notificationHandler);
|
||||
|
||||
// retrieve a list of BatchDefinitions
|
||||
List<BatchDefinition> batchDefinitionList = ConvertToBatchDefinitionList(sqlScript, startLineColumns,
|
||||
lengths);
|
||||
|
||||
return batchDefinitionList;
|
||||
}
|
||||
|
||||
|
||||
#region ExecutionEngine Event Handlers
|
||||
|
||||
private void OnBatchParserExecutionError(object sender, BatchParserExecutionErrorEventArgs args)
|
||||
{
|
||||
if (args != null)
|
||||
{
|
||||
|
||||
Logger.Write(LogLevel.Verbose, SR.BatchParserWrapperExecutionError);
|
||||
throw new Exception(SR.BatchParserWrapperExecutionEngineError);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBatchParserExecutionFinished(object sender, BatchParserExecutionFinishedEventArgs args)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (args != null && args.Batch != null)
|
||||
{
|
||||
|
||||
Tuple<int /*startLine*/, int/*startColumn*/> position = new Tuple<int, int>(args.Batch.TextSpan.iStartLine, args.Batch.TextSpan.iStartIndex);
|
||||
|
||||
|
||||
// PS168371
|
||||
//
|
||||
// There is a bug in the batch parser where it appends a '\n' to the end of the last
|
||||
// batch if a GO or statement appears at the end of the string without a \r\n. This is
|
||||
// throwing off length calculations in other places in the code because the length of
|
||||
// the string returned is longer than the length of the actual string
|
||||
//
|
||||
// To work around this issue we detect this case (a single \n without a preceding \r
|
||||
// and then adjust the length accordingly
|
||||
string batchText = args.Batch.Text;
|
||||
int batchTextLength = batchText.Length;
|
||||
|
||||
if (!batchText.EndsWith(Environment.NewLine, StringComparison.Ordinal)
|
||||
&& batchText.EndsWith("\n", StringComparison.Ordinal))
|
||||
{
|
||||
batchTextLength -= 1;
|
||||
}
|
||||
|
||||
// Add the script info
|
||||
startLineColumns.Add(position);
|
||||
lengths.Add(batchTextLength);
|
||||
}
|
||||
}
|
||||
catch (NotImplementedException)
|
||||
{
|
||||
// intentionally swallow
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// adding this for debugging
|
||||
Logger.Write(LogLevel.Warning, "Exception Caught in BatchParserWrapper.OnBatchParserExecutionFinished(...)" + e.ToString());
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Internal BatchEventHandlers class
|
||||
|
||||
/// <summary>
|
||||
/// Internal implementation class to implement IBatchEventHandlers
|
||||
/// </summary>
|
||||
internal class BatchEventNotificationHandler : IBatchEventsHandler
|
||||
{
|
||||
public void OnBatchError(object sender, BatchErrorEventArgs args)
|
||||
{
|
||||
if (args != null)
|
||||
{
|
||||
|
||||
Logger.Write(LogLevel.Normal, SR.BatchParserWrapperExecutionEngineError);
|
||||
throw new Exception(SR.BatchParserWrapperExecutionEngineError);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public void OnBatchMessage(object sender, BatchMessageEventArgs args)
|
||||
{
|
||||
#if DEBUG
|
||||
if (args != null)
|
||||
{
|
||||
Logger.Write(LogLevel.Normal, SR.BatchParserWrapperExecutionEngineBatchMessage);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public void OnBatchResultSetProcessing(object sender, BatchResultSetEventArgs args)
|
||||
{
|
||||
#if DEBUG
|
||||
if (args != null && args.DataReader != null)
|
||||
{
|
||||
Logger.Write(LogLevel.Normal, SR.BatchParserWrapperExecutionEngineBatchResultSetProcessing);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public void OnBatchResultSetFinished(object sender, EventArgs args)
|
||||
{
|
||||
#if DEBUG
|
||||
Logger.Write(LogLevel.Normal, SR.BatchParserWrapperExecutionEngineBatchResultSetFinished);
|
||||
#endif
|
||||
}
|
||||
|
||||
public void OnBatchCancelling(object sender, EventArgs args)
|
||||
{
|
||||
Logger.Write(LogLevel.Normal, SR.BatchParserWrapperExecutionEngineBatchCancelling);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable implementation
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
if (executionEngine != null)
|
||||
{
|
||||
executionEngine.Dispose();
|
||||
executionEngine = null;
|
||||
startLineColumns = null;
|
||||
lengths = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.BatchParser
|
||||
{
|
||||
/// <summary>This exception is used to report that can come from the Batch Parser inside BatchParserWrapper.</summary>
|
||||
internal sealed class BatchParserWrapperException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Get description of the BatchParserWrapperException
|
||||
/// </summary>
|
||||
public string Description { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
27
src/Microsoft.SqlTools.ServiceLayer/BatchParser/ErrorCode.cs
Normal file
27
src/Microsoft.SqlTools.ServiceLayer/BatchParser/ErrorCode.cs
Normal 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.
|
||||
//
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.BatchParser
|
||||
{
|
||||
internal enum ErrorCode
|
||||
{
|
||||
ErrorCodeBase = 0,
|
||||
|
||||
Success = ErrorCodeBase,
|
||||
// Lexer error codes
|
||||
UnsupportedCommand = ErrorCodeBase + 1,
|
||||
UnrecognizedToken = ErrorCodeBase + 2,
|
||||
StringNotTerminated = ErrorCodeBase + 3,
|
||||
CommentNotTerminated = ErrorCodeBase + 4,
|
||||
|
||||
// Parser error codes
|
||||
InvalidVariableName = ErrorCodeBase + 6,
|
||||
InvalidNumber = ErrorCodeBase + 7,
|
||||
TokenExpected = ErrorCodeBase + 8,
|
||||
Aborted = ErrorCodeBase + 9,
|
||||
CircularReference = ErrorCodeBase + 10,
|
||||
VariableNotDefined = ErrorCodeBase + 11,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,873 @@
|
||||
//
|
||||
// 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;
|
||||
using System.Data.SqlClient;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection;
|
||||
using Microsoft.SqlTools.ServiceLayer.Utility;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
|
||||
{
|
||||
/// <summary>
|
||||
/// Single batch of SQL command
|
||||
/// </summary>
|
||||
internal class Batch
|
||||
{
|
||||
#region Private methods
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to format the provided SqlError
|
||||
/// </summary>
|
||||
/// <param name="error"></param>
|
||||
/// <returns></returns>
|
||||
private string FormatSqlErrorMessage(SqlError error)
|
||||
{
|
||||
string detailedMessage = string.Empty;
|
||||
|
||||
if (error.Class > 10)
|
||||
{
|
||||
if (string.IsNullOrEmpty(error.Procedure))
|
||||
{
|
||||
detailedMessage = string.Format(CultureInfo.CurrentCulture, SR.EE_BatchSqlMessageNoProcedureInfo,
|
||||
error.Number,
|
||||
error.Class,
|
||||
error.State,
|
||||
error.LineNumber);
|
||||
}
|
||||
else
|
||||
{
|
||||
detailedMessage = string.Format(CultureInfo.CurrentCulture, SR.EE_BatchSqlMessageWithProcedureInfo,
|
||||
error.Number,
|
||||
error.Class,
|
||||
error.State,
|
||||
error.Procedure,
|
||||
error.LineNumber);
|
||||
}
|
||||
}
|
||||
else if (error.Class > 0 && error.Number > 0)
|
||||
{
|
||||
detailedMessage = string.Format(CultureInfo.CurrentCulture, SR.EE_BatchSqlMessageNoLineInfo,
|
||||
error.Number,
|
||||
error.Class,
|
||||
error.State);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(detailedMessage) && !isSuppressProviderMessageHeaders)
|
||||
{
|
||||
detailedMessage = string.Format(CultureInfo.CurrentCulture, "{0}: {1}", error.Source, detailedMessage);
|
||||
}
|
||||
|
||||
return detailedMessage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles a Sql exception
|
||||
/// </summary>
|
||||
/// <param name="ex"></param>
|
||||
/// <returns></returns>
|
||||
private ScriptExecutionResult HandleSqlException(SqlException ex)
|
||||
{
|
||||
ScriptExecutionResult result;
|
||||
|
||||
lock (this)
|
||||
{
|
||||
if (state == BatchState.Cancelling)
|
||||
{
|
||||
result = ScriptExecutionResult.Cancel;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = ScriptExecutionResult.Failure;
|
||||
}
|
||||
}
|
||||
|
||||
if (result != ScriptExecutionResult.Cancel)
|
||||
{
|
||||
HandleSqlMessages(ex.Errors);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when an error message came from SqlClient
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="description"></param>
|
||||
/// <param name="line"></param>
|
||||
/// <param name="textSpan"></param>
|
||||
private void RaiseBatchError(string message, SqlError error, TextSpan textSpan)
|
||||
{
|
||||
BatchErrorEventArgs args = new BatchErrorEventArgs(message, error, textSpan, null);
|
||||
RaiseBatchError(args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when an error message came from SqlClient
|
||||
/// </summary>
|
||||
/// <param name="e"></param>
|
||||
private void RaiseBatchError(BatchErrorEventArgs e)
|
||||
{
|
||||
EventHandler<BatchErrorEventArgs> cache = BatchError;
|
||||
if (cache != null)
|
||||
{
|
||||
cache(this, e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a message came from SqlClient
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Additionally, it's being used to notify the user that the script execution
|
||||
/// has been finished.
|
||||
/// </remarks>
|
||||
/// <param name="detailedMessage"></param>
|
||||
/// <param name="message"></param>
|
||||
private void RaiseBatchMessage(string detailedMessage, string message, SqlError error)
|
||||
{
|
||||
EventHandler<BatchMessageEventArgs> cache = BatchMessage;
|
||||
if (cache != null)
|
||||
{
|
||||
BatchMessageEventArgs args = new BatchMessageEventArgs(detailedMessage, message, error);
|
||||
cache(this, args);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a new result set has to be processed
|
||||
/// </summary>
|
||||
/// <param name="resultSet"></param>
|
||||
private void RaiseBatchResultSetProcessing(IDataReader dataReader, ShowPlanType expectedShowPlan)
|
||||
{
|
||||
EventHandler<BatchResultSetEventArgs> cache = BatchResultSetProcessing;
|
||||
if (cache != null)
|
||||
{
|
||||
BatchResultSetEventArgs args = new BatchResultSetEventArgs(dataReader, expectedShowPlan);
|
||||
BatchResultSetProcessing(this, args);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the result set has been processed
|
||||
/// </summary>
|
||||
private void RaiseBatchResultSetFinished()
|
||||
{
|
||||
EventHandler<EventArgs> cache = BatchResultSetFinished;
|
||||
if (cache != null)
|
||||
{
|
||||
cache(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the batch is being cancelled with an active result set
|
||||
/// </summary>
|
||||
private void RaiseCancelling()
|
||||
{
|
||||
EventHandler<EventArgs> cache = BatchCancelling;
|
||||
if (cache != null)
|
||||
{
|
||||
cache(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Private enums
|
||||
|
||||
private enum BatchState
|
||||
{
|
||||
Initial,
|
||||
Executing,
|
||||
Executed,
|
||||
ProcessingResults,
|
||||
Cancelling,
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Private fields
|
||||
|
||||
// correspond to public properties
|
||||
private bool isSuppressProviderMessageHeaders;
|
||||
private bool isResultExpected = false;
|
||||
private string sqlText = string.Empty;
|
||||
private int execTimeout = 30;
|
||||
private int scriptTrackingId = 0;
|
||||
private bool isScriptExecutionTracked = false;
|
||||
private const int ChangeDatabase = 0x1645;
|
||||
|
||||
//command that will be used for execution
|
||||
private IDbCommand command = null;
|
||||
|
||||
//current object state
|
||||
private BatchState state = BatchState.Initial;
|
||||
|
||||
//script text to be executed
|
||||
private TextSpan textSpan;
|
||||
|
||||
//index of the batch in collection of batches
|
||||
private int index = 0;
|
||||
|
||||
private long totalAffectedRows = 0;
|
||||
|
||||
private bool hasErrors;
|
||||
|
||||
// Expected showplan if any
|
||||
private ShowPlanType expectedShowPlan;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Default constructor
|
||||
/// </summary>
|
||||
public Batch()
|
||||
{
|
||||
// nothing
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and initializes a batch object
|
||||
/// </summary>
|
||||
/// <param name="isResultExpected">Whether it is one of "set [something] on/off" type of command,
|
||||
/// that doesn't return any results from the server
|
||||
/// </param>
|
||||
/// <param name="sqlText">Text of the batch</param>
|
||||
/// <param name="execTimeout">Timeout for the batch execution. 0 means no limit </param>
|
||||
public Batch(string sqlText, bool isResultExpected, int execTimeout)
|
||||
{
|
||||
this.isResultExpected = isResultExpected;
|
||||
this.sqlText = sqlText;
|
||||
this.execTimeout = execTimeout;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public properties
|
||||
|
||||
/// <summary>
|
||||
/// Is the Batch's text valid?
|
||||
/// </summary>
|
||||
public bool HasValidText
|
||||
{
|
||||
get
|
||||
{
|
||||
return !string.IsNullOrEmpty(sqlText);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SQL text that to be executed in the Batch
|
||||
/// </summary>
|
||||
public string Text
|
||||
{
|
||||
get
|
||||
{
|
||||
return sqlText;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
sqlText = value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether batch execution returns any results
|
||||
/// </summary>
|
||||
public bool IsResultsExpected
|
||||
{
|
||||
get
|
||||
{
|
||||
return isResultExpected;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
isResultExpected = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the execution timeout for the batch
|
||||
/// </summary>
|
||||
public int ExecutionTimeout
|
||||
{
|
||||
get
|
||||
{
|
||||
return execTimeout;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
execTimeout = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the textspan to wich the batch belongs to
|
||||
/// </summary>
|
||||
public TextSpan TextSpan
|
||||
{
|
||||
get
|
||||
{
|
||||
return textSpan;
|
||||
}
|
||||
set
|
||||
{
|
||||
textSpan = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the batch index in the collection of batches being executed
|
||||
/// </summary>
|
||||
public int BatchIndex
|
||||
{
|
||||
get
|
||||
{
|
||||
return index;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
index = value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns how many rows were affected. It should be the value that can be shown
|
||||
/// in the UI.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// It can be used only after the execution of the batch is finished
|
||||
/// </remarks>
|
||||
public long RowsAffected
|
||||
{
|
||||
get
|
||||
{
|
||||
return totalAffectedRows;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the error.Source should be used when messages are written
|
||||
/// </summary>
|
||||
public bool IsSuppressProviderMessageHeaders
|
||||
{
|
||||
get
|
||||
{
|
||||
return isSuppressProviderMessageHeaders;
|
||||
}
|
||||
set
|
||||
{
|
||||
isSuppressProviderMessageHeaders = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the id of the script we are tracking
|
||||
/// </summary>
|
||||
public int ScriptTrackingId
|
||||
{
|
||||
get
|
||||
{
|
||||
return scriptTrackingId;
|
||||
}
|
||||
set
|
||||
{
|
||||
scriptTrackingId = value;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public events
|
||||
|
||||
/// <summary>
|
||||
/// fired when there is an error message from the server
|
||||
/// </summary>
|
||||
public event EventHandler<BatchErrorEventArgs> BatchError = null;
|
||||
|
||||
/// <summary>
|
||||
/// fired when there is a message from the server
|
||||
/// </summary>
|
||||
public event EventHandler<BatchMessageEventArgs> BatchMessage = null;
|
||||
|
||||
/// <summary>
|
||||
/// fired when there is a new result set available. It is guarnteed
|
||||
/// to be fired from the same thread that called Execute method
|
||||
/// </summary>
|
||||
public event EventHandler<BatchResultSetEventArgs> BatchResultSetProcessing = null;
|
||||
|
||||
/// <summary>
|
||||
/// fired when the batch recieved cancel request BEFORE it
|
||||
/// initiates cancel operation. Note that it is fired from a
|
||||
/// different thread then the one used to kick off execution
|
||||
/// </summary>
|
||||
public event EventHandler<EventArgs> BatchCancelling = null;
|
||||
|
||||
/// <summary>
|
||||
/// fired when we've done absolutely all actions for the current result set
|
||||
/// </summary>
|
||||
public event EventHandler<EventArgs> BatchResultSetFinished = null;
|
||||
#endregion
|
||||
|
||||
#region Public methods
|
||||
|
||||
/// <summary>
|
||||
/// Resets the object to its initial state
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
state = BatchState.Initial;
|
||||
command = null;
|
||||
textSpan = new TextSpan();
|
||||
totalAffectedRows = 0;
|
||||
hasErrors = false;
|
||||
expectedShowPlan = ShowPlanType.None;
|
||||
isSuppressProviderMessageHeaders = false;
|
||||
scriptTrackingId = 0;
|
||||
isScriptExecutionTracked = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the batch
|
||||
/// </summary>
|
||||
/// <param name="connection">Connection to use</param>
|
||||
/// <param name="expectedShowPlan">ShowPlan type to be used</param>
|
||||
/// <returns>result of execution</returns>
|
||||
/// <remarks>
|
||||
/// It does not return until execution is finished
|
||||
/// We may have received a Cancel request by the time this function is called
|
||||
/// </remarks>
|
||||
public ScriptExecutionResult Execute(SqlConnection connection, ShowPlanType expectedShowPlan)
|
||||
{
|
||||
// FUTURE CLEANUP: Remove in favor of general signature (IDbConnection) - #920978
|
||||
return Execute((IDbConnection)connection, expectedShowPlan);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the batch
|
||||
/// </summary>
|
||||
/// <param name="connection">Connection to use</param>
|
||||
/// <param name="expectedShowPlan">ShowPlan type to be used</param>
|
||||
/// <returns>result of execution</returns>
|
||||
/// <remarks>
|
||||
/// It does not return until execution is finished
|
||||
/// We may have received a Cancel request by the time this function is called
|
||||
/// </remarks>
|
||||
public ScriptExecutionResult Execute(IDbConnection connection, ShowPlanType expectedShowPlan)
|
||||
{
|
||||
|
||||
Validate.IsNotNull(nameof(connection), connection);
|
||||
|
||||
//makes sure that the batch is not in use
|
||||
lock (this)
|
||||
{
|
||||
Debug.Assert(command == null, "SQLCommand is NOT null");
|
||||
if (command != null)
|
||||
{
|
||||
command = null;
|
||||
}
|
||||
}
|
||||
|
||||
this.expectedShowPlan = expectedShowPlan;
|
||||
|
||||
return DoBatchExecutionImpl(connection, sqlText);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the batch
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When batch is actually cancelled, Execute() will return with the appropiate status
|
||||
/// </remarks>
|
||||
public void Cancel()
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
if (state != BatchState.Cancelling)
|
||||
{
|
||||
state = BatchState.Cancelling;
|
||||
|
||||
RaiseCancelling();
|
||||
|
||||
if (command != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
command.Cancel();
|
||||
|
||||
Debug.WriteLine("Batch.Cancel: command.Cancel completed");
|
||||
}
|
||||
catch (SqlException)
|
||||
{
|
||||
// eat it
|
||||
}
|
||||
catch (RetryLimitExceededException)
|
||||
{
|
||||
// eat it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Protected methods
|
||||
|
||||
/// <summary>
|
||||
/// Fires an error message event
|
||||
/// </summary>
|
||||
/// <param name="ex">Exception caught</param>
|
||||
/// <remarks>
|
||||
/// Non-SQL exception
|
||||
/// </remarks>
|
||||
protected void HandleExceptionMessage(Exception ex)
|
||||
{
|
||||
BatchErrorEventArgs args = new BatchErrorEventArgs(string.Format(CultureInfo.CurrentCulture, SR.EE_BatchError_Exception, ex.Message), ex);
|
||||
RaiseBatchError(args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fires a message event
|
||||
/// </summary>
|
||||
/// <param name="errors">SqlClient errors collection</param>
|
||||
/// <remarks>
|
||||
/// Sql specific messages.
|
||||
/// </remarks>
|
||||
protected void HandleSqlMessages(SqlErrorCollection errors)
|
||||
{
|
||||
foreach (SqlError error in errors)
|
||||
{
|
||||
if (error.Number == ChangeDatabase)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string detailedMessage = FormatSqlErrorMessage(error);
|
||||
|
||||
if (error.Class > 10)
|
||||
{
|
||||
// expose this event as error
|
||||
Debug.Assert(detailedMessage.Length != 0);
|
||||
RaiseBatchError(detailedMessage, error, textSpan);
|
||||
|
||||
//at least one error message has been used
|
||||
hasErrors = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
RaiseBatchMessage(detailedMessage, error.Message, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// method that will be passed as delegate to SqlConnection.InfoMessage
|
||||
/// </summary>
|
||||
protected void OnSqlInfoMessageCallback(object sender, SqlInfoMessageEventArgs e)
|
||||
{
|
||||
HandleSqlMessages(e.Errors);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delegete for SqlCommand.RecordsAffected
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="e"></param>
|
||||
/// <remarks>
|
||||
/// This is exposed as a regular message
|
||||
/// </remarks>
|
||||
protected void OnStatementExecutionFinished(object sender, StatementCompletedEventArgs e)
|
||||
{
|
||||
string message = string.Format(CultureInfo.CurrentCulture, SR.EE_BatchExecutionInfo_RowsAffected,
|
||||
e.RecordCount.ToString(System.Globalization.CultureInfo.InvariantCulture));
|
||||
RaiseBatchMessage(message, message, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on a new ResultSet on the data reader
|
||||
/// </summary>
|
||||
/// <param name="dataReader">True if result set consumed, false on a Cancel request</param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// The GridStorageResultSet created is owned by the batch consumer. It's only created here.
|
||||
/// Additionally, when BatchResultSet event handler is called, it won't return until
|
||||
/// all data is prcessed or the data being processed is terminated (i.e. cancel or error)
|
||||
/// </remarks>
|
||||
protected ScriptExecutionResult ProcessResultSet(IDataReader dataReader)
|
||||
{
|
||||
if (dataReader == null)
|
||||
{
|
||||
throw new ArgumentNullException();
|
||||
}
|
||||
|
||||
Debug.WriteLine("ProcessResultSet: result set has been created");
|
||||
|
||||
//initialize result variable that will be set by batch consumer
|
||||
ScriptExecutionResult scriptExecutionResult = ScriptExecutionResult.Success;
|
||||
|
||||
RaiseBatchResultSetProcessing(dataReader, expectedShowPlan);
|
||||
|
||||
if (state != BatchState.Cancelling)
|
||||
{
|
||||
return scriptExecutionResult;
|
||||
}
|
||||
else
|
||||
{
|
||||
return ScriptExecutionResult.Cancel;
|
||||
}
|
||||
}
|
||||
|
||||
// FUTURE CLEANUP: Remove in favor of general signature (IDbConnection) - #920978
|
||||
protected ScriptExecutionResult DoBatchExecution(SqlConnection connection, string script)
|
||||
{
|
||||
return DoBatchExecutionImpl(connection, script);
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities"), SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
|
||||
[SuppressMessage("Microsoft.Usage", "CA2219:DoNotRaiseExceptionsInExceptionClauses")]
|
||||
private ScriptExecutionResult DoBatchExecutionImpl(IDbConnection connection, string script)
|
||||
{
|
||||
Validate.IsNotNull(nameof(connection), connection);
|
||||
|
||||
lock (this)
|
||||
{
|
||||
if (state == BatchState.Cancelling)
|
||||
{
|
||||
state = BatchState.Initial;
|
||||
return ScriptExecutionResult.Cancel;
|
||||
}
|
||||
}
|
||||
|
||||
ScriptExecutionResult result = ScriptExecutionResult.Success;
|
||||
|
||||
// SqlClient event handlers setup
|
||||
SqlInfoMessageEventHandler messageHandler = new SqlInfoMessageEventHandler(OnSqlInfoMessageCallback);
|
||||
StatementCompletedEventHandler statementCompletedHandler = null;
|
||||
|
||||
DbConnectionWrapper connectionWrapper = new DbConnectionWrapper(connection);
|
||||
connectionWrapper.InfoMessage += messageHandler;
|
||||
|
||||
IDbCommand command = connection.CreateCommand();
|
||||
command.CommandText = script;
|
||||
command.CommandTimeout = execTimeout;
|
||||
|
||||
DbCommandWrapper commandWrapper = null;
|
||||
if (isScriptExecutionTracked && DbCommandWrapper.IsSupportedCommand(command))
|
||||
{
|
||||
statementCompletedHandler = new StatementCompletedEventHandler(OnStatementExecutionFinished);
|
||||
commandWrapper = new DbCommandWrapper(command);
|
||||
commandWrapper.StatementCompleted += statementCompletedHandler;
|
||||
}
|
||||
|
||||
lock (this)
|
||||
{
|
||||
state = BatchState.Executing;
|
||||
this.command = command;
|
||||
command = null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
result = this.ExecuteCommand();
|
||||
}
|
||||
catch (OutOfMemoryException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (SqlException sqlEx)
|
||||
{
|
||||
result = HandleSqlException(sqlEx);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result = ScriptExecutionResult.Failure;
|
||||
HandleExceptionMessage(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
if (messageHandler == null)
|
||||
{
|
||||
Logger.Write(LogLevel.Error, "Expected handler to be declared");
|
||||
}
|
||||
|
||||
if (null != connectionWrapper)
|
||||
{
|
||||
connectionWrapper.InfoMessage -= messageHandler;
|
||||
}
|
||||
|
||||
if (commandWrapper != null)
|
||||
{
|
||||
|
||||
if (statementCompletedHandler == null)
|
||||
{
|
||||
Logger.Write(LogLevel.Error, "Expect handler to be declared if we have a command wrapper");
|
||||
}
|
||||
commandWrapper.StatementCompleted -= statementCompletedHandler;
|
||||
}
|
||||
|
||||
lock (this)
|
||||
{
|
||||
state = BatchState.Initial;
|
||||
if (command != null)
|
||||
{
|
||||
command.Dispose();
|
||||
command = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private ScriptExecutionResult ExecuteCommand()
|
||||
{
|
||||
if (command == null)
|
||||
{
|
||||
throw new ArgumentNullException("command");
|
||||
}
|
||||
|
||||
return this.ExecuteUnTrackedCommand();
|
||||
|
||||
}
|
||||
|
||||
private ScriptExecutionResult ExecuteUnTrackedCommand()
|
||||
{
|
||||
IDataReader reader = null;
|
||||
|
||||
if (!isResultExpected)
|
||||
{
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
else
|
||||
{
|
||||
reader = command.ExecuteReader(CommandBehavior.SequentialAccess);
|
||||
}
|
||||
|
||||
return this.CheckStateAndRead(reader);
|
||||
}
|
||||
|
||||
private ScriptExecutionResult CheckStateAndRead(IDataReader reader = null)
|
||||
{
|
||||
ScriptExecutionResult result = ScriptExecutionResult.Success;
|
||||
|
||||
if (!isResultExpected)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
if (state == BatchState.Cancelling)
|
||||
{
|
||||
result = ScriptExecutionResult.Cancel;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = ScriptExecutionResult.Success;
|
||||
state = BatchState.Executed;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
if (state == BatchState.Cancelling)
|
||||
{
|
||||
result = ScriptExecutionResult.Cancel;
|
||||
}
|
||||
else
|
||||
{
|
||||
state = BatchState.ProcessingResults;
|
||||
}
|
||||
}
|
||||
|
||||
if (result != ScriptExecutionResult.Cancel)
|
||||
{
|
||||
ScriptExecutionResult batchExecutionResult = ScriptExecutionResult.Success;
|
||||
|
||||
if (reader != null)
|
||||
{
|
||||
bool hasNextResult = false;
|
||||
do
|
||||
{
|
||||
// if there were no results coming from the server, then the FieldCount is 0
|
||||
if (reader.FieldCount <= 0)
|
||||
{
|
||||
hasNextResult = reader.NextResult();
|
||||
continue;
|
||||
}
|
||||
|
||||
batchExecutionResult = ProcessResultSet(reader);
|
||||
|
||||
if (batchExecutionResult != ScriptExecutionResult.Success)
|
||||
{
|
||||
result = batchExecutionResult;
|
||||
break;
|
||||
}
|
||||
|
||||
RaiseBatchResultSetFinished();
|
||||
|
||||
hasNextResult = reader.NextResult();
|
||||
|
||||
} while (hasNextResult);
|
||||
}
|
||||
|
||||
if (hasErrors)
|
||||
{
|
||||
Debug.WriteLine("DoBatchExecution: successfull processed result set, but there were errors shown to the user");
|
||||
result = ScriptExecutionResult.Failure;
|
||||
}
|
||||
|
||||
if (result != ScriptExecutionResult.Cancel)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
state = BatchState.Executed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (reader != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// reader.Close() doesn't actually close the reader
|
||||
// so explicitly dispose the reader
|
||||
reader.Dispose();
|
||||
reader = null;
|
||||
}
|
||||
catch (OutOfMemoryException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (SqlException)
|
||||
{
|
||||
// nothing
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
|
||||
{
|
||||
///<summary>
|
||||
/// Class to get text from the BatchParser and convert them into batches
|
||||
///</summary>
|
||||
public class BatchDefinition
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Constructor method for a BatchDefinition
|
||||
/// </summary>
|
||||
public BatchDefinition(string batchText, int startLine, int endLine, int startColumn, int endColumn)
|
||||
{
|
||||
BatchText = batchText;
|
||||
StartLine = startLine;
|
||||
EndLine = endLine;
|
||||
StartColumn = startColumn;
|
||||
EndColumn = endColumn;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get starting line of the BatchDefinition
|
||||
/// </summary>
|
||||
public int StartLine
|
||||
{
|
||||
get; private set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get ending line of the BatchDefinition
|
||||
/// </summary>
|
||||
public int EndLine
|
||||
{
|
||||
get; private set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get starting column of the BatchDefinition
|
||||
/// </summary>
|
||||
public int StartColumn
|
||||
{
|
||||
get; private set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get ending column of the BatchDefinition
|
||||
/// </summary>
|
||||
public int EndColumn
|
||||
{
|
||||
get; private set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get batch text assocaited with the BatchDefinition
|
||||
/// </summary>
|
||||
public string BatchText
|
||||
{
|
||||
get; private set;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.Data.SqlClient;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
|
||||
{
|
||||
/// <summary>
|
||||
/// Error totalAffectedRows for a Batch
|
||||
/// </summary>
|
||||
internal class BatchErrorEventArgs : EventArgs
|
||||
{
|
||||
#region Private Fields
|
||||
private string message = string.Empty;
|
||||
private string description = string.Empty;
|
||||
private int line = -1;
|
||||
private TextSpan textSpan;
|
||||
private Exception exception;
|
||||
private SqlError error;
|
||||
#endregion
|
||||
|
||||
#region Constructors / Destructor
|
||||
|
||||
/// <summary>
|
||||
/// Default constructor
|
||||
/// </summary>
|
||||
private BatchErrorEventArgs()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with message and no description
|
||||
/// </summary>
|
||||
internal BatchErrorEventArgs(string message)
|
||||
: this(message, null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with exception and no description
|
||||
/// </summary>
|
||||
internal BatchErrorEventArgs(string message, Exception ex)
|
||||
: this(message, string.Empty, ex)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with message and description
|
||||
/// </summary>
|
||||
internal BatchErrorEventArgs(string message, string description, Exception ex)
|
||||
: this(message, description, -1, new TextSpan(), ex)
|
||||
{
|
||||
}
|
||||
|
||||
internal BatchErrorEventArgs(string message, SqlError error, TextSpan textSpan, Exception ex)
|
||||
{
|
||||
string desc = error != null ? error.Message : null;
|
||||
if (error.Number == 7202)
|
||||
{
|
||||
desc += " " + Environment.NewLine + SR.TroubleshootingAssistanceMessage;
|
||||
}
|
||||
|
||||
int lineNumber = error != null ? error.LineNumber : -1;
|
||||
Init(message, desc, lineNumber, textSpan, ex);
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with message, description, textspan and line number
|
||||
/// </summary>
|
||||
internal BatchErrorEventArgs(string message, string description, int line, TextSpan textSpan, Exception ex)
|
||||
{
|
||||
Init(message, description, line, textSpan, ex);
|
||||
}
|
||||
|
||||
private void Init(string message, string description, int line, TextSpan textSpan, Exception ex)
|
||||
{
|
||||
this.message = message;
|
||||
this.description = description;
|
||||
this.line = line;
|
||||
this.textSpan = textSpan;
|
||||
exception = ex;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public properties
|
||||
|
||||
public string Message
|
||||
{
|
||||
get
|
||||
{
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
public string Description
|
||||
{
|
||||
get
|
||||
{
|
||||
return description;
|
||||
}
|
||||
}
|
||||
|
||||
public int Line
|
||||
{
|
||||
get
|
||||
{
|
||||
return line;
|
||||
}
|
||||
}
|
||||
|
||||
public TextSpan TextSpan
|
||||
{
|
||||
get
|
||||
{
|
||||
return textSpan;
|
||||
}
|
||||
}
|
||||
|
||||
public Exception Exception
|
||||
{
|
||||
get
|
||||
{
|
||||
return exception;
|
||||
}
|
||||
}
|
||||
|
||||
public SqlError Error
|
||||
{
|
||||
get { return error; }
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.Data.SqlClient;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
|
||||
{
|
||||
/// <summary>
|
||||
/// Event args for notification about non-error message
|
||||
/// </summary>
|
||||
internal class BatchMessageEventArgs : EventArgs
|
||||
{
|
||||
private readonly string message = string.Empty;
|
||||
private readonly string detailedMessage = string.Empty;
|
||||
private readonly SqlError error;
|
||||
|
||||
private BatchMessageEventArgs()
|
||||
{
|
||||
}
|
||||
|
||||
internal BatchMessageEventArgs(string msg)
|
||||
: this(string.Empty, msg)
|
||||
{
|
||||
}
|
||||
|
||||
internal BatchMessageEventArgs(string detailedMsg, string msg) : this(detailedMsg, msg, null)
|
||||
{
|
||||
}
|
||||
internal BatchMessageEventArgs(string detailedMsg, string msg, SqlError error)
|
||||
{
|
||||
message = msg;
|
||||
detailedMessage = detailedMsg;
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public string Message
|
||||
{
|
||||
get
|
||||
{
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
public string DetailedMessage
|
||||
{
|
||||
get
|
||||
{
|
||||
return detailedMessage;
|
||||
}
|
||||
}
|
||||
|
||||
public SqlError Error { get { return error; } }
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
|
||||
{
|
||||
/// <summary>
|
||||
/// Class that parses queries into batches
|
||||
/// </summary>
|
||||
internal class BatchParser :
|
||||
ICommandHandler,
|
||||
IVariableResolver
|
||||
{
|
||||
#region Private fields
|
||||
protected ScriptMessageDelegate scriptMessageDelegate;
|
||||
protected ScriptErrorDelegate scriptErrorDelegate;
|
||||
protected ExecuteDelegate executeDelegate;
|
||||
protected HaltParserDelegate haltParserDelegate;
|
||||
private int startingLine = 0;
|
||||
protected bool variableSubstitutionDisabled = false;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public delegates
|
||||
public delegate void HaltParserDelegate();
|
||||
public delegate void ScriptMessageDelegate(string message);
|
||||
public delegate void ScriptErrorDelegate(string message, ScriptMessageType messageType);
|
||||
public delegate bool ExecuteDelegate(string batchScript, int num, int lineNumber);
|
||||
#endregion
|
||||
|
||||
#region Constructors / Destructor
|
||||
public BatchParser()
|
||||
{
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public properties
|
||||
public ScriptMessageDelegate Message
|
||||
{
|
||||
get { return scriptMessageDelegate; }
|
||||
set { scriptMessageDelegate = value; }
|
||||
}
|
||||
|
||||
public ScriptErrorDelegate ErrorMessage
|
||||
{
|
||||
get { return scriptErrorDelegate; }
|
||||
set { scriptErrorDelegate = value; }
|
||||
}
|
||||
|
||||
public ExecuteDelegate Execute
|
||||
{
|
||||
get { return executeDelegate; }
|
||||
set { executeDelegate = value; }
|
||||
}
|
||||
|
||||
public HaltParserDelegate HaltParser
|
||||
{
|
||||
get { return haltParserDelegate; }
|
||||
set { haltParserDelegate = value; }
|
||||
}
|
||||
|
||||
public int StartingLine
|
||||
{
|
||||
get { return startingLine; }
|
||||
set { startingLine = value; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ICommandHandler Members
|
||||
|
||||
/// <summary>
|
||||
/// Take approptiate action on the parsed batches
|
||||
/// </summary>
|
||||
public BatchParserAction Go(TextBlock batch, int repeatCount)
|
||||
{
|
||||
string str;
|
||||
LineInfo lineInfo;
|
||||
|
||||
batch.GetText(!variableSubstitutionDisabled, out str, out lineInfo);
|
||||
|
||||
bool executeResult = false;
|
||||
if (executeDelegate != null)
|
||||
{
|
||||
executeResult = executeDelegate(str, repeatCount, lineInfo.GetStreamPositionForOffset(0).Line + startingLine - 1);
|
||||
}
|
||||
return executeResult ? BatchParserAction.Continue : BatchParserAction.Abort;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Protected methods
|
||||
/// <summary>
|
||||
/// Called when the script parsing has errors/warnings
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="messageType"></param>
|
||||
protected void RaiseScriptError(string message, ScriptMessageType messageType)
|
||||
{
|
||||
if (scriptErrorDelegate != null)
|
||||
{
|
||||
scriptErrorDelegate(message, messageType);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on parsing info message
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="messageType"></param>
|
||||
protected void RaiseScriptMessage(string message)
|
||||
{
|
||||
if (scriptMessageDelegate != null)
|
||||
{
|
||||
scriptMessageDelegate(message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on parsing info message
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="messageType"></param>
|
||||
protected void RaiseHaltParser()
|
||||
{
|
||||
if (haltParserDelegate != null)
|
||||
{
|
||||
haltParserDelegate();
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
public virtual BatchParserAction OnError(Token token, OnErrorAction action)
|
||||
{
|
||||
throw new NotImplementedException("The method or operation is not implemented.");
|
||||
}
|
||||
|
||||
public virtual BatchParserAction Include(TextBlock filename, out System.IO.TextReader stream, out string newFilename)
|
||||
{
|
||||
throw new NotImplementedException("The method or operation is not implemented.");
|
||||
}
|
||||
|
||||
public virtual string GetVariable(PositionStruct pos, string name)
|
||||
{
|
||||
throw new NotImplementedException("The method or operation is not implemented.");
|
||||
}
|
||||
|
||||
public virtual void SetVariable(PositionStruct pos, string name, string value)
|
||||
{
|
||||
throw new NotImplementedException("The method or operation is not implemented.");
|
||||
}
|
||||
|
||||
internal void DisableVariableSubstitution()
|
||||
{
|
||||
variableSubstitutionDisabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
|
||||
{
|
||||
/// <summary>
|
||||
/// Class associated with batch parser execution errors
|
||||
/// </summary>
|
||||
internal class BatchParserExecutionErrorEventArgs : BatchErrorEventArgs
|
||||
{
|
||||
private readonly ScriptMessageType messageType;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor method for BatchParserExecutionErrorEventArgs class
|
||||
/// </summary>
|
||||
public BatchParserExecutionErrorEventArgs(string errorLine, string message, ScriptMessageType messageType)
|
||||
: base(errorLine, message, null)
|
||||
{
|
||||
this.messageType = messageType;
|
||||
}
|
||||
|
||||
public ScriptMessageType MessageType
|
||||
{
|
||||
get
|
||||
{
|
||||
return messageType;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// 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.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
|
||||
{
|
||||
/// <summary>
|
||||
/// Class associated with batch parser execution finished event
|
||||
/// </summary>
|
||||
internal class BatchParserExecutionFinishedEventArgs : EventArgs
|
||||
{
|
||||
|
||||
private readonly Batch batch = null;
|
||||
private readonly ScriptExecutionResult result;
|
||||
|
||||
private BatchParserExecutionFinishedEventArgs()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor method for the class
|
||||
/// </summary>
|
||||
public BatchParserExecutionFinishedEventArgs(ScriptExecutionResult batchResult, Batch batch)
|
||||
{
|
||||
this.batch = batch;
|
||||
result = batchResult;
|
||||
}
|
||||
|
||||
public Batch Batch
|
||||
{
|
||||
get
|
||||
{
|
||||
return batch;
|
||||
}
|
||||
}
|
||||
|
||||
public ScriptExecutionResult ExecutionResult
|
||||
{
|
||||
get
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// 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.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Class associated with batch parser execution start event
|
||||
/// </summary>
|
||||
internal class BatchParserExecutionStartEventArgs : EventArgs
|
||||
{
|
||||
|
||||
private readonly Batch batch = null;
|
||||
private readonly TextSpan textSpan;
|
||||
|
||||
private BatchParserExecutionStartEventArgs()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contructor method for the class
|
||||
/// </summary>
|
||||
public BatchParserExecutionStartEventArgs(TextSpan textSpan, Batch batch)
|
||||
{
|
||||
this.batch = batch;
|
||||
this.textSpan = textSpan;
|
||||
}
|
||||
|
||||
public Batch Batch
|
||||
{
|
||||
get
|
||||
{
|
||||
return batch;
|
||||
}
|
||||
}
|
||||
|
||||
public TextSpan TextSpan
|
||||
{
|
||||
get
|
||||
{
|
||||
return textSpan;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
//
|
||||
// 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.SqlClient;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
|
||||
{
|
||||
/// <summary>
|
||||
/// Class for handling SQL CMD by Batch Parser
|
||||
/// </summary>
|
||||
internal class BatchParserSqlCmd : BatchParser
|
||||
{
|
||||
/// <summary>
|
||||
/// The internal variables that can be used in SqlCommand substitution.
|
||||
/// These variables take precedence over environment variables.
|
||||
/// </summary>
|
||||
private Dictionary<string, string> internalVariables = new Dictionary<string, string>(StringComparer.CurrentCultureIgnoreCase);
|
||||
private ConnectionChangedDelegate connectionChangedDelegate;
|
||||
private ErrorActionChangedDelegate errorActionChangedDelegate;
|
||||
|
||||
public delegate void ConnectionChangedDelegate(SqlConnectionStringBuilder connectionstringBuilder);
|
||||
public delegate void ErrorActionChangedDelegate(OnErrorAction ea);
|
||||
|
||||
/// <summary>
|
||||
/// Constructor taking a Parser instance
|
||||
/// </summary>
|
||||
/// <param name="parser"></param>
|
||||
public BatchParserSqlCmd()
|
||||
: base()
|
||||
{
|
||||
// nothing
|
||||
}
|
||||
|
||||
internal ConnectionChangedDelegate ConnectionChanged
|
||||
{
|
||||
get { return connectionChangedDelegate; }
|
||||
set { connectionChangedDelegate = value; }
|
||||
}
|
||||
|
||||
internal ErrorActionChangedDelegate ErrorActionChanged
|
||||
{
|
||||
get { return errorActionChangedDelegate; }
|
||||
set { errorActionChangedDelegate = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks for any environment variable or internal variable.
|
||||
/// </summary>
|
||||
public override string GetVariable(PositionStruct pos, string name)
|
||||
{
|
||||
if (variableSubstitutionDisabled)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string value;
|
||||
|
||||
// Internally defined variables have higher precedence over environment variables.
|
||||
if (!internalVariables.TryGetValue(name, out value))
|
||||
{
|
||||
value = Environment.GetEnvironmentVariables()[name] as string;
|
||||
}
|
||||
if (value == null)
|
||||
{
|
||||
RaiseScriptError(string.Format(CultureInfo.CurrentCulture, SR.EE_ExecutionError_VariableNotFound, name), ScriptMessageType.FatalError);
|
||||
RaiseHaltParser();
|
||||
// TODO: Halt the parser, should get/set variable have ParserAction.Abort/Continue (like original?)
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set environment or internal variable
|
||||
/// </summary>
|
||||
public override void SetVariable(PositionStruct pos, string name, string value)
|
||||
{
|
||||
if (variableSubstitutionDisabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (value == null)
|
||||
{
|
||||
if (internalVariables.ContainsKey(name))
|
||||
{
|
||||
internalVariables.Remove(name);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
internalVariables[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
public Dictionary<string, string> InternalVariables
|
||||
{
|
||||
get { return internalVariables; }
|
||||
set { internalVariables = value; }
|
||||
}
|
||||
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "ppIBatchSource")]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "fileName")]
|
||||
public override BatchParserAction Include(TextBlock filename, out TextReader stream, out string newFilename)
|
||||
{
|
||||
stream = null;
|
||||
newFilename = null;
|
||||
|
||||
RaiseScriptError(string.Format(CultureInfo.CurrentCulture, SR.EE_ExecutionError_CommandNotSupported, "Include"), ScriptMessageType.Error);
|
||||
return BatchParserAction.Abort;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method to deal with errors
|
||||
/// </summary>
|
||||
public override BatchParserAction OnError(Token token, OnErrorAction ea)
|
||||
{
|
||||
if (errorActionChangedDelegate != null)
|
||||
{
|
||||
errorActionChangedDelegate(ea);
|
||||
}
|
||||
return BatchParserAction.Continue;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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 System;
|
||||
using System.Data;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
|
||||
{
|
||||
/// <summary>
|
||||
/// Class associated with setting batch results
|
||||
/// </summary>
|
||||
internal class BatchResultSetEventArgs : EventArgs
|
||||
{
|
||||
|
||||
private readonly IDataReader dataReader = null;
|
||||
private readonly ShowPlanType expectedShowPlan = ShowPlanType.None;
|
||||
|
||||
/// <summary>
|
||||
/// Default constructor
|
||||
/// </summary>
|
||||
/// <param name="dataReader"></param>
|
||||
internal BatchResultSetEventArgs(IDataReader dataReader, ShowPlanType expectedShowPlan)
|
||||
{
|
||||
this.dataReader = dataReader;
|
||||
this.expectedShowPlan = expectedShowPlan;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Data reader associated with the result set
|
||||
/// </summary>
|
||||
public IDataReader DataReader
|
||||
{
|
||||
get
|
||||
{
|
||||
return dataReader;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show Plan to be expected if any during the execution
|
||||
/// </summary>
|
||||
public ShowPlanType ExpectedShowPlan
|
||||
{
|
||||
get
|
||||
{
|
||||
return expectedShowPlan;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,277 @@
|
||||
//
|
||||
// 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.Specialized;
|
||||
using Microsoft.SqlTools.ServiceLayer.Utility;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
|
||||
{
|
||||
internal class ExecutionEngineConditions
|
||||
{
|
||||
|
||||
#region Private fields
|
||||
private static class Consts
|
||||
{
|
||||
public static string On = "ON";
|
||||
public static string Off = "OFF";
|
||||
public static string ParseOnly = "SET PARSEONLY {0}";
|
||||
public static string NoExec = "SET NOEXEC {0}";
|
||||
public static string StatisticsIO = "SET STATISTICS IO {0}";
|
||||
public static string StatisticsTime = "SET STATISTICS TIME {0}";
|
||||
public static string ShowPlanXml = "SET SHOWPLAN_XML {0}";
|
||||
public static string ShowPlanAll = "SET SHOWPLAN_ALL {0}";
|
||||
public static string ShowPlanText = "SET SHOWPLAN_TEXT {0}";
|
||||
public static string StatisticsXml = "SET STATISTICS XML {0}";
|
||||
public static string StatisticsProfile = "SET STATISTICS PROFILE {0}";
|
||||
public static string BeginTrans = "BEGIN TRAN";
|
||||
public static string CommitTrans = "COMMIT TRAN";
|
||||
public static string Rollback = "ROLLBACK";
|
||||
public static string BatchSeparator = "GO";
|
||||
|
||||
public static string Reset = "SET NOEXEC, FMTONLY OFF, PARSEONLY, SET SHOWPLAN_ALL, SET SHOWPLAN_TEXT";
|
||||
}
|
||||
|
||||
private static readonly int stateParseOnly = BitVector32.CreateMask();
|
||||
private static readonly int stateTransactionWrapped = BitVector32.CreateMask(stateParseOnly);
|
||||
private static readonly int stateHaltOnError = BitVector32.CreateMask(stateTransactionWrapped);
|
||||
private static readonly int stateEstimatedShowPlan = BitVector32.CreateMask(stateHaltOnError);
|
||||
private static readonly int stateActualShowPlan = BitVector32.CreateMask(stateEstimatedShowPlan);
|
||||
private static readonly int stateSuppressProviderMessageHeaders = BitVector32.CreateMask(stateActualShowPlan);
|
||||
private static readonly int stateNoExec = BitVector32.CreateMask(stateSuppressProviderMessageHeaders);
|
||||
private static readonly int stateStatisticsIO = BitVector32.CreateMask(stateNoExec);
|
||||
private static readonly int stateShowPlanText = BitVector32.CreateMask(stateStatisticsIO);
|
||||
private static readonly int stateStatisticsTime = BitVector32.CreateMask(stateShowPlanText);
|
||||
private static readonly int stateSqlCmd = BitVector32.CreateMask(stateStatisticsTime);
|
||||
private static readonly int stateScriptExecutionTracked = BitVector32.CreateMask(stateSqlCmd);
|
||||
|
||||
private BitVector32 state = new BitVector32();
|
||||
private string batchSeparator = Consts.BatchSeparator;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors / Destructor
|
||||
|
||||
/// <summary>
|
||||
/// Default constructor
|
||||
/// </summary>
|
||||
public ExecutionEngineConditions()
|
||||
{
|
||||
// nothing
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overloaded constructor taking another ExecutionEngineCondition object as a reference
|
||||
/// </summary>
|
||||
public ExecutionEngineConditions(ExecutionEngineConditions condition)
|
||||
{
|
||||
state = condition.state;
|
||||
batchSeparator = condition.batchSeparator;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Statement strings
|
||||
|
||||
public static string ShowPlanXmlStatement(bool isOn)
|
||||
{
|
||||
return string.Format(System.Globalization.CultureInfo.InvariantCulture, Consts.ShowPlanXml, (isOn ? Consts.On : Consts.Off));
|
||||
}
|
||||
|
||||
public static string ShowPlanAllStatement(bool isOn)
|
||||
{
|
||||
return string.Format(System.Globalization.CultureInfo.InvariantCulture, Consts.ShowPlanAll, (isOn ? Consts.On : Consts.Off));
|
||||
}
|
||||
|
||||
public static string ShowPlanTextStatement(bool isOn)
|
||||
{
|
||||
return string.Format(System.Globalization.CultureInfo.InvariantCulture, Consts.ShowPlanText, (isOn ? Consts.On : Consts.Off));
|
||||
}
|
||||
|
||||
public static string StatisticsXmlStatement(bool isOn)
|
||||
{
|
||||
return string.Format(System.Globalization.CultureInfo.InvariantCulture, Consts.StatisticsXml, (isOn ? Consts.On : Consts.Off));
|
||||
}
|
||||
|
||||
public static string StatisticsProfileStatement(bool isOn)
|
||||
{
|
||||
return string.Format(System.Globalization.CultureInfo.InvariantCulture, Consts.StatisticsProfile, (isOn ? Consts.On : Consts.Off));
|
||||
}
|
||||
|
||||
public static string ParseOnlyStatement(bool isOn)
|
||||
{
|
||||
return string.Format(System.Globalization.CultureInfo.InvariantCulture, Consts.ParseOnly, (isOn ? Consts.On : Consts.Off));
|
||||
}
|
||||
|
||||
public static string NoExecStatement(bool isOn)
|
||||
{
|
||||
return string.Format(System.Globalization.CultureInfo.InvariantCulture, Consts.NoExec, (isOn ? Consts.On : Consts.Off));
|
||||
}
|
||||
|
||||
public static string StatisticsIOStatement(bool isOn)
|
||||
{
|
||||
return string.Format(System.Globalization.CultureInfo.InvariantCulture, Consts.StatisticsIO, (isOn ? Consts.On : Consts.Off));
|
||||
}
|
||||
|
||||
public static string StatisticsTimeStatement(bool isOn)
|
||||
{
|
||||
return string.Format(System.Globalization.CultureInfo.InvariantCulture, Consts.StatisticsTime, (isOn ? Consts.On : Consts.Off));
|
||||
}
|
||||
|
||||
public static string BeginTransactionStatement
|
||||
{
|
||||
get { return Consts.BeginTrans; }
|
||||
}
|
||||
|
||||
public static string CommitTransactionStatement
|
||||
{
|
||||
get { return Consts.CommitTrans; }
|
||||
}
|
||||
|
||||
public static string RollbackTransactionStatement
|
||||
{
|
||||
get { return Consts.Rollback; }
|
||||
}
|
||||
|
||||
public static string BatchSeparatorStatement
|
||||
{
|
||||
get { return Consts.BatchSeparator; }
|
||||
}
|
||||
|
||||
public static string ResetStatement
|
||||
{
|
||||
get { return Consts.Reset; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public properties
|
||||
|
||||
/// <summary>
|
||||
/// Checks the syntax of each Transact-SQL statement and returns any error messages
|
||||
/// without compiling or executing the statement.
|
||||
/// </summary>
|
||||
public bool IsParseOnly
|
||||
{
|
||||
get { return state[stateParseOnly]; }
|
||||
set { state[stateParseOnly] = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Script is wrapped withint BEGIN TRAN/COMMIT-ROLLBACK
|
||||
/// </summary>
|
||||
public bool IsTransactionWrapped
|
||||
{
|
||||
get { return state[stateTransactionWrapped]; }
|
||||
set { state[stateTransactionWrapped] = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or a set a value indicating whether script execution is tracked
|
||||
/// </summary>
|
||||
public bool IsScriptExecutionTracked
|
||||
{
|
||||
get { return state[stateScriptExecutionTracked]; }
|
||||
set { state[stateScriptExecutionTracked] = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Halts the execution if an error is found
|
||||
/// </summary>
|
||||
public bool IsHaltOnError
|
||||
{
|
||||
get { return state[stateHaltOnError]; }
|
||||
set { state[stateHaltOnError] = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use estimated show plan
|
||||
/// </summary>
|
||||
public bool IsEstimatedShowPlan
|
||||
{
|
||||
get { return state[stateEstimatedShowPlan]; }
|
||||
set { state[stateEstimatedShowPlan] = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use actual show plan
|
||||
/// </summary>
|
||||
public bool IsActualShowPlan
|
||||
{
|
||||
get { return state[stateActualShowPlan]; }
|
||||
set { state[stateActualShowPlan] = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use Source information on messages shown to the user
|
||||
/// </summary>
|
||||
public bool IsSuppressProviderMessageHeaders
|
||||
{
|
||||
get { return state[stateSuppressProviderMessageHeaders]; }
|
||||
set { state[stateSuppressProviderMessageHeaders] = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SET NO EXEC {on/off}
|
||||
/// </summary>
|
||||
public bool IsNoExec
|
||||
{
|
||||
get { return state[stateNoExec]; }
|
||||
set { state[stateNoExec] = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SET STATISTICS IO {on/off}
|
||||
/// </summary>
|
||||
public bool IsStatisticsIO
|
||||
{
|
||||
get { return state[stateStatisticsIO]; }
|
||||
set { state[stateStatisticsIO] = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SET SHOWPLAN_TEXT {on/off}
|
||||
/// </summary>
|
||||
public bool IsShowPlanText
|
||||
{
|
||||
get { return state[stateShowPlanText]; }
|
||||
set { state[stateShowPlanText] = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SET STATISTICS IO {on/off}
|
||||
/// </summary>
|
||||
public bool IsStatisticsTime
|
||||
{
|
||||
get { return state[stateStatisticsTime]; }
|
||||
set { state[stateStatisticsTime] = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SqlCmd support
|
||||
/// </summary>
|
||||
public bool IsSqlCmd
|
||||
{
|
||||
get { return state[stateSqlCmd]; }
|
||||
set { state[stateSqlCmd] = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch separator statement
|
||||
/// </summary>
|
||||
public string BatchSeparator
|
||||
{
|
||||
get
|
||||
{
|
||||
return batchSeparator;
|
||||
}
|
||||
set
|
||||
{
|
||||
Validate.IsNotNullOrEmptyString(nameof(value), value);
|
||||
batchSeparator = value;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
|
||||
{
|
||||
internal enum ExecutionState
|
||||
{
|
||||
Initial,
|
||||
Executing,
|
||||
ExecutingBatch,
|
||||
Cancelling,
|
||||
Discarded
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// 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.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
|
||||
{
|
||||
internal interface IBatchEventsHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// fired when there is an error message from the server
|
||||
/// </summary>
|
||||
void OnBatchError(object sender, BatchErrorEventArgs args);
|
||||
|
||||
/// <summary>
|
||||
/// fired when there is a message from the server
|
||||
/// </summary>
|
||||
void OnBatchMessage(object sender, BatchMessageEventArgs args);
|
||||
|
||||
/// <summary>
|
||||
/// fired when there is a new result set available. It is guarnteed
|
||||
/// to be fired from the same thread that called Execute method
|
||||
/// </summary>
|
||||
void OnBatchResultSetProcessing(object sender, BatchResultSetEventArgs args);
|
||||
|
||||
/// <summary>
|
||||
/// fired when we've done absolutely all actions for the current result set
|
||||
/// </summary>
|
||||
void OnBatchResultSetFinished(object sender, EventArgs args);
|
||||
|
||||
/// <summary>
|
||||
/// fired when the batch recieved cancel request BEFORE it
|
||||
/// initiates cancel operation. Note that it is fired from a
|
||||
/// different thread then the one used to kick off execution
|
||||
/// </summary>
|
||||
void OnBatchCancelling(object sender, EventArgs args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
//
|
||||
// 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.SqlClient;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
|
||||
{
|
||||
internal class ScriptExecutionArgs : EventArgs
|
||||
{
|
||||
private IDbConnection connection;
|
||||
private IBatchEventsHandler batchEventHandlers;
|
||||
private int startingLine;
|
||||
private Dictionary<string, string> cmdVariables;
|
||||
|
||||
#region Constructors / Destructor
|
||||
|
||||
/// <summary>
|
||||
/// Constructor method for ScriptExecutionArgs
|
||||
/// </summary>
|
||||
public ScriptExecutionArgs(
|
||||
string script,
|
||||
SqlConnection connection,
|
||||
int timeOut,
|
||||
ExecutionEngineConditions conditions,
|
||||
IBatchEventsHandler batchEventHandlers)
|
||||
: this (script, (IDbConnection)connection, timeOut, conditions, batchEventHandlers)
|
||||
{
|
||||
// nothing
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor method for ScriptExecutionArgs
|
||||
/// </summary>
|
||||
public ScriptExecutionArgs(
|
||||
string script,
|
||||
SqlConnection connection,
|
||||
int timeOut,
|
||||
ExecutionEngineConditions conditions,
|
||||
IBatchEventsHandler batchEventHandlers,
|
||||
int startingLine,
|
||||
IDictionary<string,string> variables)
|
||||
: this(script, (IDbConnection) connection, timeOut, conditions, batchEventHandlers, startingLine, variables)
|
||||
{
|
||||
// nothing
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor method for ScriptExecutionArgs
|
||||
/// </summary>
|
||||
public ScriptExecutionArgs(
|
||||
string script,
|
||||
IDbConnection connection,
|
||||
int timeOut,
|
||||
ExecutionEngineConditions conditions,
|
||||
IBatchEventsHandler batchEventHandlers)
|
||||
: this(script, connection, timeOut, conditions, batchEventHandlers, 0, null)
|
||||
{
|
||||
// nothing
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor method for ScriptExecutionArgs
|
||||
/// </summary>
|
||||
public ScriptExecutionArgs(
|
||||
string script,
|
||||
IDbConnection connection,
|
||||
int timeOut,
|
||||
ExecutionEngineConditions conditions,
|
||||
IBatchEventsHandler batchEventHandlers,
|
||||
int startingLine,
|
||||
IDictionary<string, string> variables)
|
||||
{
|
||||
Script = script;
|
||||
this.connection = connection;
|
||||
TimeOut = timeOut;
|
||||
Conditions = conditions;
|
||||
this.batchEventHandlers = batchEventHandlers;
|
||||
this.startingLine = startingLine;
|
||||
|
||||
if (variables != null)
|
||||
{
|
||||
foreach (var variable in variables)
|
||||
{
|
||||
Variables[variable.Key] = variable.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public properties
|
||||
|
||||
public string Script { get; set; }
|
||||
|
||||
// FUTURE CLEANUP: Remove in favor of general signature (IDbConnection) - #920978
|
||||
public SqlConnection Connection
|
||||
{
|
||||
get { return connection as SqlConnection; }
|
||||
set { connection = value as SqlConnection; }
|
||||
}
|
||||
|
||||
public IDbConnection ReliableConnection
|
||||
{
|
||||
get { return connection; }
|
||||
set { connection = value; }
|
||||
}
|
||||
|
||||
public int TimeOut { get; set; }
|
||||
|
||||
internal ExecutionEngineConditions Conditions { get; set; }
|
||||
|
||||
internal IBatchEventsHandler BatchEventHandlers
|
||||
{
|
||||
get { return batchEventHandlers; }
|
||||
set { batchEventHandlers = value; }
|
||||
}
|
||||
|
||||
internal int StartingLine
|
||||
{
|
||||
get { return startingLine; }
|
||||
set { startingLine = value; }
|
||||
}
|
||||
|
||||
internal Dictionary<string, string> Variables
|
||||
{
|
||||
get
|
||||
{
|
||||
if (cmdVariables == null)
|
||||
{
|
||||
cmdVariables = new Dictionary<string, string>(StringComparer.CurrentCultureIgnoreCase);
|
||||
}
|
||||
|
||||
return cmdVariables;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// 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.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
|
||||
{
|
||||
internal class ScriptExecutionFinishedEventArgs : EventArgs
|
||||
{
|
||||
internal ScriptExecutionFinishedEventArgs(ScriptExecutionResult result)
|
||||
{
|
||||
ExecutionResult = result;
|
||||
}
|
||||
|
||||
public ScriptExecutionResult ExecutionResult
|
||||
{
|
||||
private set;
|
||||
get;
|
||||
}
|
||||
|
||||
public bool Disposing
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// 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.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
|
||||
{
|
||||
[Flags]
|
||||
internal enum ScriptExecutionResult
|
||||
{
|
||||
Success = 0x1,
|
||||
Failure = 0x2,
|
||||
Cancel = 0x4,
|
||||
Halted = 0x8,
|
||||
All = 0x0F
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
|
||||
{
|
||||
internal enum ScriptMessageType
|
||||
{
|
||||
FatalError,
|
||||
Error,
|
||||
Warning
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
|
||||
{
|
||||
[System.Flags]
|
||||
internal enum ShowPlanType
|
||||
{
|
||||
None = 0x0,
|
||||
ActualExecutionShowPlan = 0x1,
|
||||
ActualXmlShowPlan = 0x2,
|
||||
EstimatedExecutionShowPlan = 0x4,
|
||||
EstimatedXmlShowPlan = 0x8,
|
||||
AllXmlShowPlan = ActualXmlShowPlan | EstimatedXmlShowPlan,
|
||||
AllExecutionShowPlan = ActualExecutionShowPlan | EstimatedExecutionShowPlan,
|
||||
AllShowPlan = AllExecutionShowPlan | AllXmlShowPlan
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
|
||||
{
|
||||
internal struct TextSpan
|
||||
{
|
||||
public int iEndIndex;
|
||||
public int iEndLine;
|
||||
public int iStartIndex;
|
||||
public int iStartLine;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// 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;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.BatchParser
|
||||
{
|
||||
internal interface ICommandHandler
|
||||
{
|
||||
BatchParserAction Go(TextBlock batch, int repeatCount);
|
||||
BatchParserAction OnError(Token token, OnErrorAction action);
|
||||
BatchParserAction Include(TextBlock filename, out TextReader stream, out string newFilename);
|
||||
}
|
||||
}
|
||||
@@ -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.SqlTools.ServiceLayer.BatchParser
|
||||
{
|
||||
internal interface IVariableResolver
|
||||
{
|
||||
string GetVariable(PositionStruct pos, string name);
|
||||
void SetVariable(PositionStruct pos, string name, string value);
|
||||
}
|
||||
}
|
||||
747
src/Microsoft.SqlTools.ServiceLayer/BatchParser/Lexer.cs
Normal file
747
src/Microsoft.SqlTools.ServiceLayer/BatchParser/Lexer.cs
Normal file
@@ -0,0 +1,747 @@
|
||||
//
|
||||
// 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.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.BatchParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Lexer class for the SMO Batch Parser
|
||||
/// </summary>
|
||||
internal sealed class Lexer : IDisposable
|
||||
{
|
||||
private LexerInput currentInput;
|
||||
private bool popInputAtNextConsume;
|
||||
private Token currentToken;
|
||||
private ErrorCode errorCode = ErrorCode.Success;
|
||||
private readonly Stack<LexerInput> inputStack = new Stack<LexerInput>();
|
||||
private Func<bool> lexerFunc;
|
||||
private TextRuleFlags textRuleFlags;
|
||||
private PositionStruct tokenBeginPosition;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for the lexer class used by SMO Batch Parser
|
||||
/// </summary>
|
||||
public Lexer(TextReader input, string name)
|
||||
{
|
||||
currentInput = new LexerInput(input, name);
|
||||
currentToken = null;
|
||||
RecognizeSqlCmdSyntax = true;
|
||||
|
||||
SetState(RuleLine);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get current token for the lexer
|
||||
/// </summary>
|
||||
public Token CurrentToken
|
||||
{
|
||||
get {
|
||||
return currentToken;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get current token type for the lexer
|
||||
/// </summary>
|
||||
public LexerTokenType CurrentTokenType
|
||||
{
|
||||
get { return currentToken.TokenType; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Consume the token
|
||||
/// </summary>
|
||||
public void ConsumeToken()
|
||||
{
|
||||
if (currentInput == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool result;
|
||||
tokenBeginPosition = new PositionStruct(currentInput.CurrentLine, currentInput.CurrentColumn, currentInput.CurrentOffset, currentInput.Filename);
|
||||
|
||||
do
|
||||
{
|
||||
if (popInputAtNextConsume)
|
||||
{
|
||||
PopAndCloseInput();
|
||||
popInputAtNextConsume = false;
|
||||
}
|
||||
do
|
||||
{
|
||||
result = lexerFunc();
|
||||
} while (result == false);
|
||||
if (CurrentTokenType == LexerTokenType.Eof)
|
||||
{
|
||||
popInputAtNextConsume = true;
|
||||
if(inputStack.Count > 0)
|
||||
{
|
||||
// report as empty NewLine token
|
||||
currentToken = new Token(
|
||||
LexerTokenType.NewLine, tokenBeginPosition, tokenBeginPosition, string.Empty, tokenBeginPosition.Filename);
|
||||
}
|
||||
}
|
||||
} while (result == false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
while (inputStack.Count > 0)
|
||||
{
|
||||
PopAndCloseInput();
|
||||
}
|
||||
}
|
||||
|
||||
public void PopAndCloseInput()
|
||||
{
|
||||
if (currentInput != null)
|
||||
{
|
||||
currentInput.Dispose();
|
||||
currentInput = null;
|
||||
}
|
||||
if (inputStack.Count > 0)
|
||||
{
|
||||
currentInput = inputStack.Pop();
|
||||
SetState(RuleLine);
|
||||
}
|
||||
}
|
||||
|
||||
static string GetCircularReferenceErrorMessage(string filename)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, SR.BatchParser_CircularReference, filename);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Push current input into the stack
|
||||
/// </summary>
|
||||
public void PushInput(TextReader reader, string name)
|
||||
{
|
||||
Debug.Assert(currentToken != null &&
|
||||
(currentToken.TokenType == LexerTokenType.NewLine || currentToken.TokenType == LexerTokenType.Eof), "New input can only be pushed after new line token or EOF");
|
||||
|
||||
if (name.Equals(currentInput.Filename, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
RaiseError(ErrorCode.CircularReference, GetCircularReferenceErrorMessage(name));
|
||||
}
|
||||
|
||||
foreach (LexerInput input in inputStack)
|
||||
{
|
||||
if (name.Equals(input.Filename, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
RaiseError(ErrorCode.CircularReference, GetCircularReferenceErrorMessage(name));
|
||||
}
|
||||
}
|
||||
|
||||
inputStack.Push(currentInput);
|
||||
currentInput = new LexerInput(reader, name);
|
||||
SetState(RuleLine);
|
||||
// We don't want to close the input, in that case the current file would be taken out of the
|
||||
// input stack and cycle detection won't work.
|
||||
popInputAtNextConsume = false;
|
||||
ConsumeToken();
|
||||
}
|
||||
|
||||
private void AcceptBlockComment()
|
||||
{
|
||||
char? ch;
|
||||
int nestingCount = 0;
|
||||
|
||||
Consume();
|
||||
do
|
||||
{
|
||||
Consume();
|
||||
ch = Lookahead();
|
||||
|
||||
if (ch.HasValue == false)
|
||||
{
|
||||
RaiseError(ErrorCode.CommentNotTerminated,
|
||||
string.Format(CultureInfo.CurrentCulture, SR.BatchParser_CommentNotTerminated));
|
||||
}
|
||||
|
||||
if (ch.Value == '*')
|
||||
{
|
||||
char? ch2 = Lookahead(1);
|
||||
if (ch2.HasValue && ch2.Value == '/')
|
||||
{
|
||||
Consume();
|
||||
if (nestingCount == 0)
|
||||
{
|
||||
Consume();
|
||||
break;
|
||||
}
|
||||
nestingCount--;
|
||||
}
|
||||
}
|
||||
else if (ch.Value == '/')
|
||||
{
|
||||
char? ch2 = Lookahead(1);
|
||||
if (ch2.HasValue && ch2.Value == '*')
|
||||
{
|
||||
Consume();
|
||||
nestingCount++;
|
||||
}
|
||||
}
|
||||
} while (true);
|
||||
SetToken(LexerTokenType.Comment);
|
||||
}
|
||||
|
||||
private void AcceptLineComment()
|
||||
{
|
||||
char? ch;
|
||||
|
||||
Consume();
|
||||
do
|
||||
{
|
||||
Consume();
|
||||
ch = Lookahead();
|
||||
|
||||
if (ch.HasValue == false || IsNewLineChar(ch.Value))
|
||||
{
|
||||
break;
|
||||
}
|
||||
} while (true);
|
||||
SetToken(LexerTokenType.Comment);
|
||||
}
|
||||
|
||||
private void AcceptIdentifier()
|
||||
{
|
||||
char? ch;
|
||||
|
||||
do
|
||||
{
|
||||
Consume();
|
||||
ch = Lookahead();
|
||||
} while (ch.HasValue && IsIdentifierChar(ch.Value));
|
||||
SetToken(LexerTokenType.Text);
|
||||
}
|
||||
|
||||
private void AcceptNewLine()
|
||||
{
|
||||
char? ch = Lookahead();
|
||||
|
||||
if (ch == '\r')
|
||||
{
|
||||
Consume();
|
||||
ch = Lookahead();
|
||||
}
|
||||
if (ch == '\n')
|
||||
{
|
||||
Consume();
|
||||
}
|
||||
|
||||
SetToken(LexerTokenType.NewLine);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method reads ahead until the closingChar is found. When closingChar is found,
|
||||
/// the next character is checked. If it's the same as closingChar, the character is
|
||||
/// escaped and the method resumes looking for a non-escaped closingChar.
|
||||
/// </summary>
|
||||
private void AcceptEscapableQuotedText(char closingChar)
|
||||
{
|
||||
char? ch;
|
||||
|
||||
while (true)
|
||||
{
|
||||
Consume();
|
||||
ch = Lookahead();
|
||||
if (!ch.HasValue)
|
||||
{
|
||||
// we reached the end without finding our closing character. that's an error.
|
||||
RaiseError(ErrorCode.StringNotTerminated, SR.BatchParser_StringNotTerminated);
|
||||
break;
|
||||
}
|
||||
|
||||
if (ch == closingChar)
|
||||
{
|
||||
// we found the closing character. we call consume to ensure the pointer is now at the subsequent character.
|
||||
Consume();
|
||||
|
||||
// Check whether the subsequent character is also closingChar.
|
||||
// If it is, that means the closingChar is escaped, so we must continue searching.
|
||||
// Otherwise, we're finished.
|
||||
char? nextChar = Lookahead();
|
||||
if (!nextChar.HasValue || nextChar != closingChar)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method reads ahead until the closingChar is found. This method does not allow for escaping
|
||||
/// of the closingChar.
|
||||
/// </summary>
|
||||
private void AcceptQuotedText(char closingChar)
|
||||
{
|
||||
char? ch;
|
||||
|
||||
do
|
||||
{
|
||||
Consume();
|
||||
ch = Lookahead();
|
||||
} while (ch.HasValue && ch != closingChar);
|
||||
|
||||
if (ch.HasValue == false)
|
||||
{
|
||||
RaiseError(ErrorCode.StringNotTerminated, SR.BatchParser_StringNotTerminated);
|
||||
}
|
||||
else
|
||||
{
|
||||
Consume();
|
||||
}
|
||||
}
|
||||
|
||||
private void AcceptWhitespace()
|
||||
{
|
||||
char? ch;
|
||||
|
||||
do
|
||||
{
|
||||
Consume();
|
||||
ch = Lookahead();
|
||||
} while (ch.HasValue && IsWhitespaceChar(ch.Value));
|
||||
|
||||
SetToken(LexerTokenType.Whitespace);
|
||||
}
|
||||
|
||||
private void Consume()
|
||||
{
|
||||
currentInput.Consume();
|
||||
}
|
||||
|
||||
private void ChangeStateToBatchCommand(Token token)
|
||||
{
|
||||
switch (token.TokenType)
|
||||
{
|
||||
case LexerTokenType.Setvar:
|
||||
SetState(RuleSetvar);
|
||||
break;
|
||||
case LexerTokenType.Go:
|
||||
SetTextState(TextRuleFlags.ReportWhitespace | TextRuleFlags.RecognizeLineComment);
|
||||
break;
|
||||
case LexerTokenType.Include:
|
||||
SetTextState(TextRuleFlags.ReportWhitespace | TextRuleFlags.RecognizeDoubleQuotedString);
|
||||
break;
|
||||
case LexerTokenType.OnError:
|
||||
SetTextState(TextRuleFlags.ReportWhitespace | TextRuleFlags.RecognizeLineComment);
|
||||
break;
|
||||
default:
|
||||
SetTextState(TextRuleFlags.ReportWhitespace);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsDigit(char ch)
|
||||
{
|
||||
return ch >= '0' && ch <= '9';
|
||||
}
|
||||
|
||||
private static bool IsLetter(char ch)
|
||||
{
|
||||
return ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z';
|
||||
}
|
||||
|
||||
private bool IsNewLineChar(char ch)
|
||||
{
|
||||
return ch == '\r' || ch == '\n';
|
||||
}
|
||||
|
||||
private bool IsWhitespaceChar(char ch)
|
||||
{
|
||||
return ch == ' ' || ch == '\t';
|
||||
}
|
||||
|
||||
private char? Lookahead()
|
||||
{
|
||||
return currentInput.Lookahead();
|
||||
}
|
||||
|
||||
private char? Lookahead(int lookahead)
|
||||
{
|
||||
return currentInput.Lookahead(lookahead);
|
||||
}
|
||||
|
||||
private bool RuleError()
|
||||
{
|
||||
// lexer repeats last error
|
||||
Parser.RaiseError(errorCode, CurrentToken);
|
||||
Debug.Fail("Parser.RaiseError should throw an exception");
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool RuleLine()
|
||||
{
|
||||
char? ch = Lookahead();
|
||||
|
||||
if (!ch.HasValue)
|
||||
{
|
||||
SetToken(LexerTokenType.Eof);
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (ch.Value)
|
||||
{
|
||||
case ' ':
|
||||
case '\t':
|
||||
AcceptWhitespace();
|
||||
return true;
|
||||
case '\r':
|
||||
case '\n':
|
||||
AcceptNewLine();
|
||||
return true;
|
||||
case ':':
|
||||
if (RecognizeSqlCmdSyntax && TryAcceptBatchCommandAndSetToken())
|
||||
{
|
||||
ChangeStateToBatchCommand(currentToken);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case 'g':
|
||||
case 'G':
|
||||
if (TryAccept("go", true))
|
||||
{
|
||||
SetToken(LexerTokenType.Go);
|
||||
ChangeStateToBatchCommand(currentToken);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
SetTextState(TextRuleFlags.RecognizeSingleQuotedString | TextRuleFlags.RecognizeDoubleQuotedString | TextRuleFlags.RecognizeLineComment | TextRuleFlags.RecognizeBlockComment | TextRuleFlags.RecognizeBrace);
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool RuleSetvar()
|
||||
{
|
||||
char? ch = Lookahead();
|
||||
|
||||
if (ch.HasValue == false)
|
||||
{
|
||||
SetToken(LexerTokenType.Eof);
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (ch.Value)
|
||||
{
|
||||
case '\r':
|
||||
case '\n':
|
||||
AcceptNewLine();
|
||||
SetState(RuleLine);
|
||||
return true;
|
||||
case ' ':
|
||||
case '\t':
|
||||
AcceptWhitespace();
|
||||
return true;
|
||||
default:
|
||||
if (IsStartIdentifierChar(ch.Value))
|
||||
{
|
||||
AcceptIdentifier();
|
||||
SetTextState(TextRuleFlags.ReportWhitespace | TextRuleFlags.RecognizeDoubleQuotedString);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// prepare error token
|
||||
do
|
||||
{
|
||||
Consume();
|
||||
ch = Lookahead();
|
||||
} while (ch != null && IsWhitespaceChar(ch.Value) == false && IsNewLineChar(ch.Value) == false);
|
||||
RaiseError(ErrorCode.UnrecognizedToken);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool RuleText()
|
||||
{
|
||||
char? ch = Lookahead();
|
||||
|
||||
if (ch.HasValue == false)
|
||||
{
|
||||
SetToken(LexerTokenType.Eof);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ch.HasValue)
|
||||
{
|
||||
if (IsNewLineChar(ch.Value))
|
||||
{
|
||||
AcceptNewLine();
|
||||
SetState(RuleLine);
|
||||
return true;
|
||||
}
|
||||
else if (textRuleFlags.HasFlag(TextRuleFlags.ReportWhitespace) && IsWhitespaceChar(ch.Value))
|
||||
{
|
||||
AcceptWhitespace();
|
||||
return true;
|
||||
}
|
||||
else if (textRuleFlags.HasFlag(TextRuleFlags.RecognizeBlockComment) || textRuleFlags.HasFlag(TextRuleFlags.RecognizeLineComment))
|
||||
{
|
||||
char? ch2 = Lookahead(1);
|
||||
if (textRuleFlags.HasFlag(TextRuleFlags.RecognizeBlockComment) && ch == '/' && ch2 == '*')
|
||||
{
|
||||
AcceptBlockComment();
|
||||
return true;
|
||||
}
|
||||
else if (textRuleFlags.HasFlag(TextRuleFlags.RecognizeLineComment) && ch == '-' && ch2 == '-')
|
||||
{
|
||||
AcceptLineComment();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (ch.HasValue)
|
||||
{
|
||||
bool consumed = false;
|
||||
switch (ch.Value)
|
||||
{
|
||||
case ' ':
|
||||
case '\t':
|
||||
if (textRuleFlags.HasFlag(TextRuleFlags.ReportWhitespace))
|
||||
{
|
||||
SetToken(LexerTokenType.Text);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case '\r':
|
||||
case '\n':
|
||||
SetToken(LexerTokenType.Text);
|
||||
return true;
|
||||
case '"':
|
||||
if (textRuleFlags.HasFlag(TextRuleFlags.RecognizeDoubleQuotedString))
|
||||
{
|
||||
AcceptQuotedText('"');
|
||||
consumed = true;
|
||||
}
|
||||
break;
|
||||
case '\'':
|
||||
if (textRuleFlags.HasFlag(TextRuleFlags.RecognizeSingleQuotedString))
|
||||
{
|
||||
AcceptEscapableQuotedText('\'');
|
||||
consumed = true;
|
||||
}
|
||||
break;
|
||||
case '[':
|
||||
if (textRuleFlags.HasFlag(TextRuleFlags.RecognizeBrace))
|
||||
{
|
||||
AcceptEscapableQuotedText(']');
|
||||
consumed = true;
|
||||
}
|
||||
break;
|
||||
case '-':
|
||||
if (textRuleFlags.HasFlag(TextRuleFlags.RecognizeLineComment))
|
||||
{
|
||||
char? ch2 = Lookahead(1);
|
||||
if (ch.HasValue && ch2 == '-')
|
||||
{
|
||||
SetToken(LexerTokenType.Text);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case '/':
|
||||
if (textRuleFlags.HasFlag(TextRuleFlags.RecognizeBlockComment))
|
||||
{
|
||||
char? ch2 = Lookahead(1);
|
||||
if (ch.HasValue && ch2 == '*')
|
||||
{
|
||||
SetToken(LexerTokenType.Text);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (consumed == false)
|
||||
{
|
||||
Consume();
|
||||
}
|
||||
ch = Lookahead();
|
||||
}
|
||||
SetToken(LexerTokenType.Text);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void RaiseError(ErrorCode code, string message = null)
|
||||
{
|
||||
SetState(RuleError);
|
||||
SetToken(LexerTokenType.Error);
|
||||
errorCode = code;
|
||||
Parser.RaiseError(errorCode, CurrentToken, message);
|
||||
}
|
||||
|
||||
private void SetState(Func<bool> lexerFunc)
|
||||
{
|
||||
this.lexerFunc = lexerFunc;
|
||||
}
|
||||
|
||||
internal void SetTextState(TextRuleFlags textRuleFlags)
|
||||
{
|
||||
this.textRuleFlags = textRuleFlags;
|
||||
SetState(RuleText);
|
||||
}
|
||||
|
||||
private void SetToken(LexerTokenType lexerTokenType)
|
||||
{
|
||||
string text = currentInput.FlushBufferedText();
|
||||
|
||||
currentToken = new Token(
|
||||
lexerTokenType,
|
||||
tokenBeginPosition,
|
||||
new PositionStruct(currentInput.CurrentLine, currentInput.CurrentColumn, currentInput.CurrentOffset, currentInput.Filename),
|
||||
text,
|
||||
currentInput.Filename);
|
||||
}
|
||||
|
||||
private bool TryAccept(string text, bool wordBoundary)
|
||||
{
|
||||
Debug.Assert(text.Length > 0);
|
||||
|
||||
int i = 0;
|
||||
do
|
||||
{
|
||||
char? ch = Lookahead(i);
|
||||
if (ch.HasValue == false || char.ToLowerInvariant(ch.Value) != text[i])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
i++;
|
||||
} while (i < text.Length);
|
||||
|
||||
if (wordBoundary)
|
||||
{
|
||||
char? ch = Lookahead(text.Length);
|
||||
if (ch != null && IsWhitespaceChar(ch.Value) == false && IsNewLineChar(ch.Value) == false
|
||||
&& ch != '$' && ch != '/' && ch != '-' && ch != '\'' && ch != '"' && ch != '(' && ch != '[' && ch != '!')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// consume all checked characters
|
||||
for (i = 0; i < text.Length; i++)
|
||||
{
|
||||
Consume();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryAcceptBatchCommandAndSetToken()
|
||||
{
|
||||
Consume(); // colon
|
||||
|
||||
if (TryAccept("reset", true))
|
||||
{
|
||||
SetToken(LexerTokenType.Reset);
|
||||
return true;
|
||||
}
|
||||
else if (TryAccept("ed", true))
|
||||
{
|
||||
SetToken(LexerTokenType.Ed);
|
||||
return true;
|
||||
}
|
||||
else if (TryAccept("!!", true))
|
||||
{
|
||||
SetToken(LexerTokenType.Execute);
|
||||
return true;
|
||||
}
|
||||
else if (TryAccept("quit", true))
|
||||
{
|
||||
SetToken(LexerTokenType.Quit);
|
||||
return true;
|
||||
}
|
||||
else if (TryAccept("exit", true))
|
||||
{
|
||||
SetToken(LexerTokenType.Exit);
|
||||
return true;
|
||||
}
|
||||
else if (TryAccept("r", true))
|
||||
{
|
||||
SetToken(LexerTokenType.Include);
|
||||
return true;
|
||||
}
|
||||
else if (TryAccept("serverlist", true))
|
||||
{
|
||||
SetToken(LexerTokenType.Serverlist);
|
||||
return true;
|
||||
}
|
||||
else if (TryAccept("setvar", true))
|
||||
{
|
||||
SetToken(LexerTokenType.Setvar);
|
||||
return true;
|
||||
}
|
||||
else if (TryAccept("list", true))
|
||||
{
|
||||
SetToken(LexerTokenType.List);
|
||||
return true;
|
||||
}
|
||||
else if (TryAccept("error", true))
|
||||
{
|
||||
SetToken(LexerTokenType.ErrorCommand);
|
||||
return true;
|
||||
}
|
||||
else if (TryAccept("out", true))
|
||||
{
|
||||
SetToken(LexerTokenType.Out);
|
||||
return true;
|
||||
}
|
||||
else if (TryAccept("perftrace", true))
|
||||
{
|
||||
SetToken(LexerTokenType.Perftrace);
|
||||
return true;
|
||||
}
|
||||
else if (TryAccept("connect", true))
|
||||
{
|
||||
SetToken(LexerTokenType.Connect);
|
||||
return true;
|
||||
}
|
||||
else if (TryAccept("on error", true))
|
||||
{
|
||||
SetToken(LexerTokenType.OnError);
|
||||
return true;
|
||||
}
|
||||
else if (TryAccept("help", true))
|
||||
{
|
||||
SetToken(LexerTokenType.Help);
|
||||
return true;
|
||||
}
|
||||
else if (TryAccept("xml", true))
|
||||
{
|
||||
SetToken(LexerTokenType.Xml);
|
||||
return true;
|
||||
}
|
||||
else if (TryAccept("listvar", true))
|
||||
{
|
||||
SetToken(LexerTokenType.ListVar);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
internal static bool IsIdentifierChar(char ch)
|
||||
{
|
||||
return IsLetter(ch) || IsDigit(ch) || ch == '_' || ch == '-';
|
||||
}
|
||||
|
||||
internal static bool IsStartIdentifierChar(char ch)
|
||||
{
|
||||
return IsLetter(ch) || ch == '_';
|
||||
}
|
||||
|
||||
public bool RecognizeSqlCmdSyntax { get; set; }
|
||||
}
|
||||
}
|
||||
186
src/Microsoft.SqlTools.ServiceLayer/BatchParser/LexerInput.cs
Normal file
186
src/Microsoft.SqlTools.ServiceLayer/BatchParser/LexerInput.cs
Normal file
@@ -0,0 +1,186 @@
|
||||
//
|
||||
// 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 System.Text;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.BatchParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Lexer input class used by lexer in SMO Batch Parser
|
||||
/// </summary>
|
||||
internal sealed class LexerInput : IDisposable
|
||||
{
|
||||
private readonly string filename;
|
||||
private TextReader input;
|
||||
private int currentLine;
|
||||
private int currentColumn;
|
||||
private int bufferStartOffset;
|
||||
private int currentSbOffset;
|
||||
private StringBuilder buffer;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor method for the LexerInput class
|
||||
/// </summary>
|
||||
public LexerInput(TextReader reader, string filename)
|
||||
{
|
||||
input = reader;
|
||||
this.filename = filename;
|
||||
currentLine = 1;
|
||||
currentColumn = 1;
|
||||
bufferStartOffset = 0;
|
||||
currentSbOffset = 0;
|
||||
buffer = new StringBuilder();
|
||||
EnsureBytes(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get filename associated with lexer input
|
||||
/// </summary>
|
||||
public string Filename
|
||||
{
|
||||
get { return filename; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get current line associated with lexer input
|
||||
/// </summary>
|
||||
public int CurrentLine
|
||||
{
|
||||
get { return currentLine; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get current column associated with lexer input
|
||||
/// </summary>
|
||||
public int CurrentColumn
|
||||
{
|
||||
get { return currentColumn; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Consume token used by lexer input
|
||||
/// </summary>
|
||||
public void Consume()
|
||||
{
|
||||
bool newLineWithCR = false;
|
||||
|
||||
char? ch = Lookahead();
|
||||
if (ch == null)
|
||||
{
|
||||
// end of stream
|
||||
return;
|
||||
}
|
||||
else if (ch == '\r')
|
||||
{
|
||||
newLineWithCR = true;
|
||||
}
|
||||
else if (ch == '\n')
|
||||
{
|
||||
currentLine++;
|
||||
currentColumn = 0;
|
||||
}
|
||||
|
||||
int count = EnsureBytes(1);
|
||||
if (count == 0)
|
||||
{
|
||||
// end of stream
|
||||
return;
|
||||
}
|
||||
currentSbOffset++;
|
||||
|
||||
if (newLineWithCR && Lookahead() != '\n')
|
||||
{
|
||||
currentLine++;
|
||||
currentColumn = 0;
|
||||
}
|
||||
currentColumn++;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (input != null)
|
||||
{
|
||||
input.Dispose();
|
||||
input = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get current offset for the lexer input
|
||||
/// </summary>
|
||||
public int CurrentOffset
|
||||
{
|
||||
get { return bufferStartOffset + currentSbOffset; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure correct number of bytes to buffer
|
||||
/// </summary>
|
||||
public int EnsureBytes(int bytesToBuffer)
|
||||
{
|
||||
if (currentSbOffset + bytesToBuffer > buffer.Length)
|
||||
{
|
||||
if (input == null)
|
||||
{
|
||||
return buffer.Length - currentSbOffset;
|
||||
}
|
||||
int chArrayLength = bytesToBuffer - (buffer.Length - currentSbOffset) + 128;
|
||||
char[] chArray = new char[chArrayLength];
|
||||
int count = input.ReadBlock(chArray, 0, chArrayLength);
|
||||
buffer.Append(chArray, 0, count);
|
||||
if (count < chArrayLength)
|
||||
{
|
||||
input.Dispose();
|
||||
input = null;
|
||||
}
|
||||
return buffer.Length - currentSbOffset;
|
||||
}
|
||||
return bytesToBuffer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// look ahead bytes in lexer input
|
||||
/// </summary>
|
||||
public char? Lookahead()
|
||||
{
|
||||
int count = EnsureBytes(1);
|
||||
if (count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return buffer[currentSbOffset];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// look ahead bytes in lexer input
|
||||
/// </summary>
|
||||
public char? Lookahead(int lookahead)
|
||||
{
|
||||
int count = EnsureBytes(lookahead + 1);
|
||||
if (count < lookahead + 1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return buffer[currentSbOffset + lookahead];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flush buffered text in lexer input
|
||||
/// </summary>
|
||||
public string FlushBufferedText()
|
||||
{
|
||||
string text;
|
||||
|
||||
text = buffer.ToString(0, currentSbOffset);
|
||||
bufferStartOffset += currentSbOffset;
|
||||
buffer.Remove(0, currentSbOffset);
|
||||
currentSbOffset = 0;
|
||||
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.BatchParser
|
||||
{
|
||||
internal enum LexerTokenType
|
||||
{
|
||||
None,
|
||||
Text,
|
||||
TextVerbatim,
|
||||
Whitespace,
|
||||
NewLine,
|
||||
String,
|
||||
Eof,
|
||||
Error,
|
||||
Comment,
|
||||
|
||||
// batch commands
|
||||
Go,
|
||||
Reset,
|
||||
Ed,
|
||||
Execute,
|
||||
Quit,
|
||||
Exit,
|
||||
Include,
|
||||
Serverlist,
|
||||
Setvar,
|
||||
List,
|
||||
ErrorCommand,
|
||||
Out,
|
||||
Perftrace,
|
||||
Connect,
|
||||
OnError,
|
||||
Help,
|
||||
Xml,
|
||||
ListVar,
|
||||
}
|
||||
}
|
||||
118
src/Microsoft.SqlTools.ServiceLayer/BatchParser/LineInfo.cs
Normal file
118
src/Microsoft.SqlTools.ServiceLayer/BatchParser/LineInfo.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
//
|
||||
// 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;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.BatchParser
|
||||
{
|
||||
/// <summary>
|
||||
/// This class gives information about lines being parsed by
|
||||
/// the Batch Parser
|
||||
/// </summary>
|
||||
class LineInfo
|
||||
{
|
||||
private IEnumerable<Token> tokens;
|
||||
private IEnumerable<VariableReference> variableRefs;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor method for the LineInfo class
|
||||
/// </summary>
|
||||
public LineInfo(IEnumerable<Token> tokens, IEnumerable<VariableReference> variableRefs)
|
||||
{
|
||||
this.tokens = tokens;
|
||||
this.variableRefs = variableRefs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the stream position for offset and returns a PositionStruct
|
||||
/// </summary>
|
||||
public PositionStruct GetStreamPositionForOffset(int offset)
|
||||
{
|
||||
if (variableRefs != null)
|
||||
{
|
||||
offset = CalculateVarsUnresolvedOffset(offset);
|
||||
}
|
||||
int charCount = 0;
|
||||
Token lastToken = null;
|
||||
foreach (Token token in tokens)
|
||||
{
|
||||
lastToken = token;
|
||||
if (charCount + token.Text.Length > offset)
|
||||
{
|
||||
int line, column;
|
||||
CalculateLineColumnForOffset(token, offset - charCount, out line, out column);
|
||||
return new PositionStruct(line, column, token.Begin.Offset + (offset - charCount), token.Filename);
|
||||
}
|
||||
charCount += token.Text.Length;
|
||||
}
|
||||
if (lastToken != null)
|
||||
{
|
||||
return new PositionStruct(lastToken.End.Line, lastToken.End.Column, lastToken.End.Offset, lastToken.Filename);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new PositionStruct(1, 1, 0, string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void CalculateLineColumnForOffset(Token token, int offset, out int line, out int column)
|
||||
{
|
||||
CalculateLineColumnForOffset(token.Text, offset, 0, token.Begin.Line, token.Begin.Column, out line, out column);
|
||||
}
|
||||
|
||||
internal static void CalculateLineColumnForOffset(string text, int offset,
|
||||
int offsetDelta, int lineDelta, int columnDelta, out int line, out int column)
|
||||
{
|
||||
line = lineDelta;
|
||||
column = columnDelta;
|
||||
int counter = offsetDelta;
|
||||
while (counter < offset)
|
||||
{
|
||||
bool newLineWithCR = false;
|
||||
|
||||
if (text[counter] == '\r')
|
||||
{
|
||||
newLineWithCR = true;
|
||||
}
|
||||
else if (text[counter] == '\n')
|
||||
{
|
||||
line++;
|
||||
column = 0;
|
||||
}
|
||||
counter++;
|
||||
if (newLineWithCR && counter < text.Length && text[counter] != '\n')
|
||||
{
|
||||
line++;
|
||||
column = 0;
|
||||
}
|
||||
column++;
|
||||
}
|
||||
}
|
||||
|
||||
private int CalculateVarsUnresolvedOffset(int offset)
|
||||
{
|
||||
// find offset of the beginning of variable substitution (if offset points to the middle of it)
|
||||
int diff = 0;
|
||||
foreach (VariableReference reference in variableRefs)
|
||||
{
|
||||
if (reference.Start >= offset)
|
||||
{
|
||||
break;
|
||||
}
|
||||
else if (reference.VariableValue != null && offset < reference.Start + reference.VariableValue.Length)
|
||||
{
|
||||
offset = reference.Start;
|
||||
break;
|
||||
}
|
||||
if (reference.VariableValue != null)
|
||||
{
|
||||
diff += reference.Length - reference.VariableValue.Length;
|
||||
}
|
||||
}
|
||||
return offset + diff;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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.SqlTools.ServiceLayer.BatchParser
|
||||
{
|
||||
internal enum OnErrorAction
|
||||
{
|
||||
Ignore = 0,
|
||||
Exit = 1,
|
||||
}
|
||||
}
|
||||
722
src/Microsoft.SqlTools.ServiceLayer/BatchParser/Parser.cs
Normal file
722
src/Microsoft.SqlTools.ServiceLayer/BatchParser/Parser.cs
Normal file
@@ -0,0 +1,722 @@
|
||||
//
|
||||
// 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.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.BatchParser
|
||||
{
|
||||
/// <summary>
|
||||
/// The Parser class on which the Batch Parser is based on
|
||||
/// </summary>
|
||||
internal sealed class Parser : IDisposable
|
||||
{
|
||||
private readonly ICommandHandler commandHandler;
|
||||
private Lexer lexer;
|
||||
private List<Token> tokenBuffer;
|
||||
private readonly IVariableResolver variableResolver;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for the Parser class
|
||||
/// </summary>
|
||||
public Parser(ICommandHandler commandHandler, IVariableResolver variableResolver, TextReader reader, string name)
|
||||
{
|
||||
this.commandHandler = commandHandler;
|
||||
this.variableResolver = variableResolver;
|
||||
lexer = new Lexer(reader, name);
|
||||
tokenBuffer = new List<Token>();
|
||||
}
|
||||
|
||||
public bool ThrowOnUnresolvedVariable { get; set; }
|
||||
|
||||
private Token LookaheadToken
|
||||
{
|
||||
get { return lexer.CurrentToken; }
|
||||
}
|
||||
|
||||
private LexerTokenType LookaheadTokenType
|
||||
{
|
||||
get { return lexer.CurrentTokenType; }
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (lexer != null)
|
||||
{
|
||||
lexer.Dispose();
|
||||
lexer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private bool AcceptWhitespaceOrComment()
|
||||
{
|
||||
bool found = false;
|
||||
while (LookaheadTokenType == LexerTokenType.Comment ||
|
||||
LookaheadTokenType == LexerTokenType.Whitespace)
|
||||
{
|
||||
Accept();
|
||||
found = true;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
private bool Accept(LexerTokenType lexerTokenType)
|
||||
{
|
||||
if (LookaheadTokenType == lexerTokenType)
|
||||
{
|
||||
Accept();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void AddTokenToStringBuffer()
|
||||
{
|
||||
Token token = LookaheadToken;
|
||||
|
||||
if (token.TokenType != LexerTokenType.Comment)
|
||||
{
|
||||
// All variable references must be valid
|
||||
CheckVariableReferences(token);
|
||||
}
|
||||
if (token.TokenType == LexerTokenType.NewLine && token.Text.Length == 0)
|
||||
{
|
||||
// Add "virtual" token representing new line that was not in original file
|
||||
PositionStruct beginPos = token.Begin;
|
||||
PositionStruct endPos = new PositionStruct(beginPos.Line + 1, 1, beginPos.Offset, beginPos.Filename);
|
||||
|
||||
tokenBuffer.Add(new Token(LexerTokenType.NewLine, beginPos, endPos, Environment.NewLine, beginPos.Filename));
|
||||
}
|
||||
else
|
||||
{
|
||||
tokenBuffer.Add(token);
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckVariableReferences(Token token)
|
||||
{
|
||||
// AddVariableReferences will raise error if reference is not constructed correctly
|
||||
AddVariableReferences(token, 0, null);
|
||||
}
|
||||
|
||||
private void AddVariableReferences(Token token, int offset, IList<VariableReference> variableRefs)
|
||||
{
|
||||
if (lexer.RecognizeSqlCmdSyntax == false)
|
||||
{
|
||||
// variables are recognized only in sqlcmd mode.
|
||||
return;
|
||||
}
|
||||
|
||||
string text = token.Text;
|
||||
|
||||
int i = 0;
|
||||
int startIndex = -1;
|
||||
int state = 0;
|
||||
while (i < text.Length)
|
||||
{
|
||||
char ch = text[i];
|
||||
if (state == 0 && ch == '$')
|
||||
{
|
||||
state = 1;
|
||||
}
|
||||
else if (state == 1)
|
||||
{
|
||||
if (ch == '(')
|
||||
{
|
||||
state = 2;
|
||||
}
|
||||
else if (ch != '$')
|
||||
{
|
||||
state = 0;
|
||||
}
|
||||
}
|
||||
else if (state == 2)
|
||||
{
|
||||
if (Lexer.IsStartIdentifierChar(ch))
|
||||
{
|
||||
startIndex = i;
|
||||
state = 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
RaiseError(ErrorCode.InvalidVariableName, GetSubToken(token, i - 2, i + 1));
|
||||
}
|
||||
}
|
||||
else if (state == 3)
|
||||
{
|
||||
if (ch == ')')
|
||||
{
|
||||
if (variableRefs != null)
|
||||
{
|
||||
variableRefs.Add(new VariableReference(offset + startIndex - 2, i - startIndex + 3, text.Substring(startIndex, i - startIndex)));
|
||||
}
|
||||
state = 0;
|
||||
}
|
||||
else if (Lexer.IsIdentifierChar(ch) == false)
|
||||
{
|
||||
RaiseError(ErrorCode.InvalidVariableName, GetSubToken(token, startIndex - 2, i + 1));
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (state == 2 || state == 3)
|
||||
{
|
||||
RaiseError(ErrorCode.InvalidVariableName, GetSubToken(token, startIndex - 2, i));
|
||||
}
|
||||
}
|
||||
|
||||
private static Token GetSubToken(Token token, int startOffset, int endOffset, LexerTokenType? newTokenType = null)
|
||||
{
|
||||
LexerTokenType tokenType = newTokenType.HasValue ? newTokenType.Value : token.TokenType;
|
||||
string text = token.Text.Substring(startOffset, endOffset - startOffset);
|
||||
string filename = token.Begin.Filename;
|
||||
PositionStruct beginPos, endPos;
|
||||
|
||||
int beginLine, beginColumn;
|
||||
LineInfo.CalculateLineColumnForOffset(token, startOffset, out beginLine, out beginColumn);
|
||||
beginPos = new PositionStruct(beginLine, beginColumn, token.Begin.Offset + startOffset, filename);
|
||||
|
||||
int endLine, endColumn;
|
||||
LineInfo.CalculateLineColumnForOffset(token, endOffset, out endLine, out endColumn);
|
||||
endPos = new PositionStruct(endLine, endColumn, token.Begin.Offset + endOffset, filename);
|
||||
|
||||
return new Token(tokenType, beginPos, endPos, text, filename);
|
||||
}
|
||||
|
||||
private LexerTokenType Accept()
|
||||
{
|
||||
lexer.ConsumeToken();
|
||||
return LookaheadTokenType;
|
||||
}
|
||||
|
||||
private void ExecuteBatch(int repeatCount)
|
||||
{
|
||||
BatchParserAction action;
|
||||
action = commandHandler.Go(new TextBlock(this, tokenBuffer), repeatCount);
|
||||
|
||||
if (action == BatchParserAction.Abort)
|
||||
{
|
||||
RaiseError(ErrorCode.Aborted);
|
||||
}
|
||||
tokenBuffer = new List<Token>();
|
||||
}
|
||||
|
||||
private bool Expect(LexerTokenType lexerTokenType)
|
||||
{
|
||||
if (LookaheadTokenType == lexerTokenType)
|
||||
return true;
|
||||
RaiseError(ErrorCode.TokenExpected);
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool ExpectAndAccept(LexerTokenType lexerTokenType)
|
||||
{
|
||||
if (Accept(lexerTokenType))
|
||||
return true;
|
||||
RaiseError(ErrorCode.TokenExpected);
|
||||
return false;
|
||||
}
|
||||
|
||||
internal void Parse()
|
||||
{
|
||||
Accept();
|
||||
ParseLines();
|
||||
}
|
||||
|
||||
private void ParseGo()
|
||||
{
|
||||
int repeatCount = 1;
|
||||
AcceptWhitespaceOrComment();
|
||||
if (LookaheadTokenType == LexerTokenType.Text)
|
||||
{
|
||||
string text = ResolveVariables(LookaheadToken, 0, null);
|
||||
for (int i = 0; i < text.Length; i++)
|
||||
{
|
||||
if (Char.IsDigit(text[i]) == false)
|
||||
{
|
||||
RaiseError(ErrorCode.InvalidNumber);
|
||||
}
|
||||
}
|
||||
bool result = Int32.TryParse(text, out repeatCount);
|
||||
if (result == false)
|
||||
{
|
||||
RaiseError(ErrorCode.InvalidNumber);
|
||||
}
|
||||
Accept(LexerTokenType.Text);
|
||||
AcceptWhitespaceOrComment();
|
||||
}
|
||||
if (LookaheadTokenType != LexerTokenType.Eof)
|
||||
{
|
||||
ExpectAndAccept(LexerTokenType.NewLine);
|
||||
}
|
||||
ExecuteBatch(repeatCount);
|
||||
}
|
||||
|
||||
private void ParseInclude()
|
||||
{
|
||||
BatchParserAction action;
|
||||
|
||||
Accept(LexerTokenType.Whitespace);
|
||||
Expect(LexerTokenType.Text);
|
||||
|
||||
Token token = LookaheadToken;
|
||||
|
||||
if (token.Text.StartsWith("--", StringComparison.Ordinal))
|
||||
{
|
||||
RaiseError(ErrorCode.TokenExpected);
|
||||
}
|
||||
|
||||
lexer.SetTextState(TextRuleFlags.ReportWhitespace | TextRuleFlags.RecognizeLineComment | TextRuleFlags.RecognizeDoubleQuotedString);
|
||||
Accept();
|
||||
AcceptWhitespaceOrComment();
|
||||
if (LookaheadTokenType != LexerTokenType.Eof)
|
||||
{
|
||||
Expect(LexerTokenType.NewLine);
|
||||
}
|
||||
TextReader textReader;
|
||||
string newFilename;
|
||||
|
||||
string tokenText = token.Text;
|
||||
IEnumerable<Token> tokens;
|
||||
if (tokenText.IndexOf('"') != -1)
|
||||
{
|
||||
tokens = SplitQuotedTextToken(token);
|
||||
}
|
||||
else
|
||||
{
|
||||
tokens = new[] { token };
|
||||
}
|
||||
|
||||
action = commandHandler.Include(new TextBlock(this, tokens), out textReader, out newFilename);
|
||||
|
||||
if (action == BatchParserAction.Abort)
|
||||
{
|
||||
RaiseError(ErrorCode.Aborted);
|
||||
}
|
||||
if (textReader != null)
|
||||
{
|
||||
this.PushInput(textReader, newFilename);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<Token> SplitQuotedTextToken(Token token)
|
||||
{
|
||||
string tokenText = token.Text;
|
||||
|
||||
if (tokenText.Length <= 1)
|
||||
{
|
||||
return new[] { token };
|
||||
}
|
||||
|
||||
IList<Token> tokens = new List<Token>();
|
||||
|
||||
int offset = 0;
|
||||
int quotePos = tokenText.IndexOf('"');
|
||||
while (quotePos != -1)
|
||||
{
|
||||
int closingQuotePos;
|
||||
if (quotePos != offset)
|
||||
{
|
||||
tokens.Add(GetSubToken(token, offset, quotePos));
|
||||
offset = quotePos;
|
||||
}
|
||||
closingQuotePos = tokenText.IndexOf('"', quotePos + 1);
|
||||
if (closingQuotePos == -1)
|
||||
{
|
||||
// odd number of " characters
|
||||
break;
|
||||
}
|
||||
if (closingQuotePos + 1 < tokenText.Length && tokenText[closingQuotePos + 1] == '"')
|
||||
{
|
||||
// two consequtive " characters: report one of them
|
||||
tokens.Add(GetSubToken(token, quotePos + 1, closingQuotePos + 1, LexerTokenType.TextVerbatim));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add the contents of the quoted string, but remove the " characters
|
||||
tokens.Add(GetSubToken(token, quotePos + 1, closingQuotePos, LexerTokenType.TextVerbatim));
|
||||
}
|
||||
offset = closingQuotePos + 1;
|
||||
|
||||
quotePos = tokenText.IndexOf('"', offset);
|
||||
}
|
||||
if (offset != tokenText.Length)
|
||||
{
|
||||
tokens.Add(GetSubToken(token, offset, tokenText.Length));
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
internal void PushInput(TextReader reader, string filename)
|
||||
{
|
||||
lexer.PushInput(reader, filename);
|
||||
}
|
||||
|
||||
private void ParseLines()
|
||||
{
|
||||
do
|
||||
{
|
||||
LexerTokenType tokenType = LookaheadTokenType;
|
||||
switch (tokenType)
|
||||
{
|
||||
case LexerTokenType.OnError:
|
||||
RemoveLastWhitespaceToken();
|
||||
Token onErrorToken = LookaheadToken;
|
||||
Accept();
|
||||
ParseOnErrorCommand(onErrorToken);
|
||||
break;
|
||||
case LexerTokenType.Eof:
|
||||
if (tokenBuffer.Count > 0)
|
||||
{
|
||||
ExecuteBatch(1);
|
||||
}
|
||||
return;
|
||||
case LexerTokenType.Go:
|
||||
RemoveLastWhitespaceToken();
|
||||
Accept();
|
||||
ParseGo();
|
||||
break;
|
||||
case LexerTokenType.Include:
|
||||
RemoveLastWhitespaceToken();
|
||||
Accept();
|
||||
ParseInclude();
|
||||
break;
|
||||
case LexerTokenType.Comment:
|
||||
case LexerTokenType.NewLine:
|
||||
case LexerTokenType.Text:
|
||||
case LexerTokenType.Whitespace:
|
||||
AddTokenToStringBuffer();
|
||||
Accept();
|
||||
break;
|
||||
case LexerTokenType.Setvar:
|
||||
Token setvarToken = LookaheadToken;
|
||||
RemoveLastWhitespaceToken();
|
||||
Accept();
|
||||
ParseSetvar(setvarToken);
|
||||
break;
|
||||
case LexerTokenType.Connect:
|
||||
case LexerTokenType.Ed:
|
||||
case LexerTokenType.ErrorCommand:
|
||||
case LexerTokenType.Execute:
|
||||
case LexerTokenType.Exit:
|
||||
case LexerTokenType.Help:
|
||||
case LexerTokenType.List:
|
||||
case LexerTokenType.ListVar:
|
||||
case LexerTokenType.Out:
|
||||
case LexerTokenType.Perftrace:
|
||||
case LexerTokenType.Quit:
|
||||
case LexerTokenType.Reset:
|
||||
case LexerTokenType.Serverlist:
|
||||
case LexerTokenType.Xml:
|
||||
RaiseError(ErrorCode.UnsupportedCommand,
|
||||
string.Format(CultureInfo.CurrentCulture, SR.EE_ExecutionError_CommandNotSupported, tokenType));
|
||||
break;
|
||||
default:
|
||||
RaiseError(ErrorCode.UnrecognizedToken);
|
||||
break;
|
||||
}
|
||||
} while (true);
|
||||
}
|
||||
|
||||
private void RemoveLastWhitespaceToken()
|
||||
{
|
||||
if (tokenBuffer.Count > 0 && tokenBuffer[tokenBuffer.Count - 1].TokenType == LexerTokenType.Whitespace)
|
||||
{
|
||||
tokenBuffer.RemoveAt(tokenBuffer.Count - 1);
|
||||
}
|
||||
}
|
||||
|
||||
private void ParseOnErrorCommand(Token onErrorToken)
|
||||
{
|
||||
ExpectAndAccept(LexerTokenType.Whitespace);
|
||||
Expect(LexerTokenType.Text);
|
||||
|
||||
string action;
|
||||
action = ResolveVariables(LookaheadToken, 0, null);
|
||||
|
||||
OnErrorAction onErrorAction;
|
||||
|
||||
if (action.Equals("exit", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
onErrorAction = OnErrorAction.Exit;
|
||||
}
|
||||
else if (action.Equals("ignore", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
onErrorAction = OnErrorAction.Ignore;
|
||||
}
|
||||
else
|
||||
{
|
||||
RaiseError(ErrorCode.UnrecognizedToken);
|
||||
return;
|
||||
}
|
||||
|
||||
Accept(LexerTokenType.Text);
|
||||
AcceptWhitespaceOrComment();
|
||||
if (LookaheadTokenType != LexerTokenType.Eof)
|
||||
{
|
||||
Expect(LexerTokenType.NewLine);
|
||||
}
|
||||
|
||||
BatchParserAction parserAction;
|
||||
|
||||
parserAction = commandHandler.OnError(onErrorToken, onErrorAction);
|
||||
|
||||
if (parserAction == BatchParserAction.Abort)
|
||||
{
|
||||
RaiseError(ErrorCode.Aborted);
|
||||
}
|
||||
}
|
||||
|
||||
private void ParseSetvar(Token setvarToken)
|
||||
{
|
||||
string variableName;
|
||||
string variableValue = null;
|
||||
Accept(LexerTokenType.Whitespace);
|
||||
Expect(LexerTokenType.Text);
|
||||
|
||||
variableName = LookaheadToken.Text;
|
||||
|
||||
Accept();
|
||||
Accept(LexerTokenType.Whitespace);
|
||||
|
||||
switch (LookaheadTokenType)
|
||||
{
|
||||
case LexerTokenType.Text:
|
||||
// No variable substitution
|
||||
variableValue = UnquoteVariableValue(LookaheadToken.Text);
|
||||
lexer.SetTextState(TextRuleFlags.ReportWhitespace | TextRuleFlags.RecognizeLineComment | TextRuleFlags.RecognizeDoubleQuotedString);
|
||||
Accept();
|
||||
AcceptWhitespaceOrComment();
|
||||
|
||||
if (LookaheadTokenType != LexerTokenType.NewLine &&
|
||||
LookaheadTokenType != LexerTokenType.Eof)
|
||||
{
|
||||
RaiseError(ErrorCode.UnrecognizedToken);
|
||||
}
|
||||
|
||||
if (LookaheadTokenType != LexerTokenType.Eof)
|
||||
{
|
||||
Accept();
|
||||
}
|
||||
break;
|
||||
case LexerTokenType.NewLine:
|
||||
case LexerTokenType.Eof:
|
||||
Accept();
|
||||
break;
|
||||
default:
|
||||
RaiseError(ErrorCode.UnrecognizedToken);
|
||||
break;
|
||||
}
|
||||
|
||||
variableResolver.SetVariable(setvarToken.Begin, variableName, variableValue);
|
||||
}
|
||||
|
||||
internal void RaiseError(ErrorCode errorCode, string message = null)
|
||||
{
|
||||
RaiseError(errorCode, LookaheadToken, message);
|
||||
}
|
||||
|
||||
internal static void RaiseError(ErrorCode errorCode, Token token, string message = null)
|
||||
{
|
||||
if (message == null)
|
||||
{
|
||||
message = string.Format(CultureInfo.CurrentCulture, SR.BatchParser_IncorrectSyntax, token.Text);
|
||||
}
|
||||
throw new BatchParserException(errorCode, token, message);
|
||||
}
|
||||
|
||||
internal string ResolveVariables(Token inputToken, int offset, List<VariableReference> variableRefs)
|
||||
{
|
||||
List<VariableReference> variableRefsLocal = new List<VariableReference>();
|
||||
AddVariableReferences(inputToken, offset, variableRefsLocal);
|
||||
|
||||
string inputString = inputToken.Text;
|
||||
if (variableRefsLocal.Count == 0)
|
||||
{
|
||||
// no variable references to substitute
|
||||
return inputString;
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
int lastChar = 0;
|
||||
PositionStruct? variablePos = null;
|
||||
foreach (VariableReference reference in variableRefsLocal)
|
||||
{
|
||||
int line;
|
||||
int column;
|
||||
|
||||
if (variablePos != null)
|
||||
{
|
||||
LineInfo.CalculateLineColumnForOffset(inputToken.Text, reference.Start - offset,
|
||||
variablePos.Value.Offset, variablePos.Value.Line, variablePos.Value.Column,
|
||||
out line, out column);
|
||||
}
|
||||
else
|
||||
{
|
||||
LineInfo.CalculateLineColumnForOffset(inputToken, reference.Start - offset, out line, out column);
|
||||
}
|
||||
|
||||
variablePos = new PositionStruct(
|
||||
line: line,
|
||||
column: column,
|
||||
offset: reference.Start - offset,
|
||||
filename: inputToken.Filename);
|
||||
string value = variableResolver.GetVariable(variablePos.Value, reference.VariableName);
|
||||
if (value == null)
|
||||
{
|
||||
// Undefined variable
|
||||
if (ThrowOnUnresolvedVariable == true)
|
||||
{
|
||||
RaiseError(ErrorCode.VariableNotDefined, inputToken,
|
||||
string.Format(CultureInfo.CurrentCulture, SR.BatchParser_VariableNotDefined, reference.VariableName));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
reference.VariableValue = value;
|
||||
sb.Append(inputToken.Text, lastChar, reference.Start - offset - lastChar);
|
||||
sb.Append(value);
|
||||
lastChar = reference.Start - offset + reference.Length;
|
||||
}
|
||||
if (lastChar != inputString.Length)
|
||||
{
|
||||
sb.Append(inputString, lastChar, inputString.Length - lastChar);
|
||||
}
|
||||
if (variableRefs != null)
|
||||
{
|
||||
variableRefs.AddRange(variableRefsLocal);
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private string UnquoteVariableValue(string s)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s) == false)
|
||||
{
|
||||
if (s.Length >= 2 && s[0] == '"' && s[s.Length - 1] == s[0])
|
||||
{
|
||||
// all " characters must be doubled
|
||||
for (int i = 1; i < s.Length - 1; i++)
|
||||
{
|
||||
if (s[i] == '"')
|
||||
{
|
||||
if (i + 1 >= s.Length - 1 || s[i + 1] != '"')
|
||||
{
|
||||
RaiseError(ErrorCode.TokenExpected);
|
||||
}
|
||||
// skip next character
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return s.Substring(1, s.Length - 2).Replace("\"\"", "\"");
|
||||
}
|
||||
else if (s[0] == '"' || s[s.Length - 1] == '"')
|
||||
{
|
||||
RaiseError(ErrorCode.TokenExpected);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (s.Length >= 2 && s[0] == '-' && s[1] == '-')
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (s.IndexOf('"') >= 0)
|
||||
{
|
||||
RaiseError(ErrorCode.TokenExpected);
|
||||
}
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
public void SetRecognizeSqlCmdSyntax(bool recognizeSqlCmdSyntax)
|
||||
{
|
||||
lexer.RecognizeSqlCmdSyntax = recognizeSqlCmdSyntax;
|
||||
}
|
||||
|
||||
public static TextReader GetBufferedTextReaderForFile(string filename)
|
||||
{
|
||||
Exception handledException = null;
|
||||
try
|
||||
{
|
||||
return new StringReader(File.ReadAllText(filename));
|
||||
}
|
||||
catch (IOException exception)
|
||||
{
|
||||
handledException = exception;
|
||||
}
|
||||
catch (NotSupportedException exception)
|
||||
{
|
||||
handledException = exception;
|
||||
}
|
||||
catch (UnauthorizedAccessException exception)
|
||||
{
|
||||
handledException = exception;
|
||||
}
|
||||
catch (SecurityException exception)
|
||||
{
|
||||
handledException = exception;
|
||||
}
|
||||
|
||||
if (null != handledException)
|
||||
{
|
||||
throw new Exception(handledException.Message, handledException);
|
||||
}
|
||||
|
||||
Debug.Fail("This code should not be reached.");
|
||||
return null;
|
||||
}
|
||||
|
||||
internal void SetBatchDelimiter(string batchSeparator)
|
||||
{
|
||||
// Not implemeneted as only GO is supported now
|
||||
}
|
||||
|
||||
public static string TokenTypeToCommandString(LexerTokenType lexerTokenType)
|
||||
{
|
||||
switch (lexerTokenType)
|
||||
{
|
||||
case LexerTokenType.Connect:
|
||||
return "Connect";
|
||||
case LexerTokenType.Ed:
|
||||
return "Ed";
|
||||
case LexerTokenType.ErrorCommand:
|
||||
return "Error";
|
||||
case LexerTokenType.Execute:
|
||||
return "!!";
|
||||
case LexerTokenType.Exit:
|
||||
return "Exit";
|
||||
case LexerTokenType.Help:
|
||||
return "Help";
|
||||
case LexerTokenType.List:
|
||||
return "List";
|
||||
case LexerTokenType.ListVar:
|
||||
return "ListVar";
|
||||
case LexerTokenType.OnError:
|
||||
return "On Error";
|
||||
case LexerTokenType.Out:
|
||||
return "Out";
|
||||
case LexerTokenType.Perftrace:
|
||||
return "PerfTrace";
|
||||
case LexerTokenType.Quit:
|
||||
return "Quit";
|
||||
case LexerTokenType.Reset:
|
||||
return "Reset";
|
||||
case LexerTokenType.Serverlist:
|
||||
return "ServerList";
|
||||
case LexerTokenType.Xml:
|
||||
return "Xml";
|
||||
default:
|
||||
Debug.Fail("Unknown batch parser command");
|
||||
return lexerTokenType.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 System;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.BatchParser
|
||||
{
|
||||
[Serializable]
|
||||
internal struct PositionStruct
|
||||
{
|
||||
private readonly int line;
|
||||
private readonly int column;
|
||||
private readonly int offset;
|
||||
private readonly string filename;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for the PositionStruct class
|
||||
/// </summary>
|
||||
public PositionStruct(int line, int column, int offset, string filename)
|
||||
{
|
||||
this.line = line;
|
||||
this.column = column;
|
||||
this.offset = offset;
|
||||
this.filename = filename;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get line from the PositionStruct
|
||||
/// </summary>
|
||||
public int Line { get { return line; } }
|
||||
|
||||
/// <summary>
|
||||
/// Get column from the PositionStruct
|
||||
/// </summary>
|
||||
public int Column { get { return column; } }
|
||||
|
||||
/// <summary>
|
||||
/// Get offset from the PositionStruct
|
||||
/// </summary>
|
||||
public int Offset { get { return offset; } }
|
||||
|
||||
/// <summary>
|
||||
/// Get file name from the PositionStruct
|
||||
/// </summary>
|
||||
public string Filename { get { return filename; } }
|
||||
}
|
||||
}
|
||||
68
src/Microsoft.SqlTools.ServiceLayer/BatchParser/TextBlock.cs
Normal file
68
src/Microsoft.SqlTools.ServiceLayer/BatchParser/TextBlock.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System.Text;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.BatchParser
|
||||
{
|
||||
internal class TextBlock
|
||||
{
|
||||
private readonly Parser parser;
|
||||
private readonly IEnumerable<Token> tokens;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for the TextBlock class
|
||||
/// </summary>
|
||||
public TextBlock(Parser parser, Token token) : this(parser, new[] { token })
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for the TextBlock class
|
||||
/// </summary>
|
||||
public TextBlock(Parser parser, IEnumerable<Token> tokens)
|
||||
{
|
||||
this.parser = parser;
|
||||
this.tokens = tokens;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get text from TextBlock
|
||||
/// </summary>
|
||||
public void GetText(bool resolveVariables, out string text, out LineInfo lineInfo)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
List<VariableReference> variableRefs = null;
|
||||
|
||||
if (resolveVariables == false)
|
||||
{
|
||||
foreach (Token token in tokens)
|
||||
{
|
||||
sb.Append(token.Text);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
variableRefs = new List<VariableReference>();
|
||||
foreach (Token token in tokens)
|
||||
{
|
||||
if (token.TokenType == LexerTokenType.Text)
|
||||
{
|
||||
sb.Append(parser.ResolveVariables(token, sb.Length, variableRefs));
|
||||
}
|
||||
else
|
||||
{
|
||||
// comments and whitespaces do not need variable expansion
|
||||
sb.Append(token.Text);
|
||||
}
|
||||
}
|
||||
}
|
||||
lineInfo = new LineInfo(tokens, variableRefs);
|
||||
text = sb.ToString();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// 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.SqlTools.ServiceLayer.BatchParser
|
||||
{
|
||||
[Flags]
|
||||
internal enum TextRuleFlags
|
||||
{
|
||||
ReportWhitespace = 1,
|
||||
RecognizeDoubleQuotedString = 2,
|
||||
RecognizeSingleQuotedString = 4,
|
||||
RecognizeLineComment = 8,
|
||||
RecognizeBlockComment = 16,
|
||||
RecognizeBrace = 32,
|
||||
}
|
||||
}
|
||||
47
src/Microsoft.SqlTools.ServiceLayer/BatchParser/Token.cs
Normal file
47
src/Microsoft.SqlTools.ServiceLayer/BatchParser/Token.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.BatchParser
|
||||
{
|
||||
internal sealed class Token
|
||||
{
|
||||
/// <summary>
|
||||
/// Token class used by the lexer in Batch Parser
|
||||
/// </summary>
|
||||
internal Token(LexerTokenType tokenType, PositionStruct begin, PositionStruct end, string text, string filename)
|
||||
{
|
||||
TokenType = tokenType;
|
||||
Begin = begin;
|
||||
End = end;
|
||||
Text = text;
|
||||
Filename = filename;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get file name associated with Token
|
||||
/// </summary>
|
||||
public string Filename { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get beginning position for the Token
|
||||
/// </summary>
|
||||
public PositionStruct Begin { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get end position for the Token
|
||||
/// </summary>
|
||||
public PositionStruct End { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get text assocaited with the Token
|
||||
/// </summary>
|
||||
public string Text { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get token type of the Token
|
||||
/// </summary>
|
||||
public LexerTokenType TokenType { get; private set; }
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.BatchParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Class for reference of variables used by the lexer
|
||||
/// </summary>
|
||||
internal sealed class VariableReference
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor method for VariableReference class
|
||||
/// </summary>
|
||||
public VariableReference(int start, int length, string variableName)
|
||||
{
|
||||
Start = start;
|
||||
Length = length;
|
||||
VariableName = variableName;
|
||||
VariableValue = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get length associated with the VariableReference
|
||||
/// </summary>
|
||||
public int Length { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get start position associated with the VariableReference
|
||||
/// </summary>
|
||||
public int Start { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get variable name associated with the VariableReference
|
||||
/// </summary>
|
||||
public string VariableName { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get variable value associated with the VariableReference
|
||||
/// </summary>
|
||||
public string VariableValue { get; internal set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user