Move managed parser into its own project (test code coverage) (#774)

* Created New ManagedBatchParser project in .NetStandard

* Addressing PR Comments

* Resolve 'No Repository' warning.

* Move batch parser tests to integrations test project

* Fix SLN file
This commit is contained in:
Karl Burtram
2019-02-07 20:13:03 -08:00
committed by GitHub
parent 0a172f3c8e
commit 022282800a
92 changed files with 2471 additions and 6391 deletions

View File

@@ -0,0 +1,13 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.SqlTools.ServiceLayer.BatchParser
{
public enum BatchParserAction
{
Continue = 0,
Abort = 1
}
}

View File

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

View File

@@ -0,0 +1,491 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.Utility;
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode;
using System.Globalization;
using System.Diagnostics;
using Microsoft.SqlTools.ManagedBatchParser;
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<BatchInfo> batchInfos;
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, List<BatchInfo> batchInfos)
{
List<BatchDefinition> batchDefinitionList = new List<BatchDefinition>();
if (batchInfos.Count == 0)
{
return batchDefinitionList;
}
List<int> offsets = GetOffsets(content, batchInfos);
if (!string.IsNullOrEmpty(content) && (batchInfos.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 = batchInfos[0].startLine + 1; //positions is 0 index based
int endLine = startLine;
int lineDifference = 0;
int endColumn;
int offset = offsets[0];
int startColumn = batchInfos[0].startColumn;
int count = batchInfos.Count;
string batchText = content.Substring(offset, batchInfos[0].length);
// if there's only one batch then the line difference is just startLine
if (count > 1)
{
lineDifference = batchInfos[1].startLine - batchInfos[0].startLine;
}
// get endLine, endColumn for the current batch and the lineStartOffset for the next batch
var position = ReadLines(reader, lineDifference, endLine);
endLine = position.Item1;
endColumn = position.Item2;
// create a new BatchDefinition and add it to the list
BatchDefinition batchDef = new BatchDefinition(
batchText,
startLine,
endLine,
startColumn + 1,
endColumn + 1,
batchInfos[0].executionCount
);
batchDefinitionList.Add(batchDef);
if (count > 1)
{
offset = offsets[1] + batchInfos[0].startColumn;
}
// Generate the rest batch definitions
for (int index = 1; index < count - 1; index++)
{
lineDifference = batchInfos[index + 1].startLine - batchInfos[index].startLine;
position = ReadLines(reader, lineDifference, endLine);
endLine = position.Item1;
endColumn = position.Item2;
offset = offsets[index];
batchText = content.Substring(offset, batchInfos[index].length);
startLine = batchInfos[index].startLine;
startColumn = batchInfos[index].startColumn;
// make a new batch definition for each batch
BatchDefinition batch = new BatchDefinition(
batchText,
startLine,
endLine,
startColumn + 1,
endColumn + 1,
batchInfos[index].executionCount
);
batchDefinitionList.Add(batch);
}
// if there is only one batch then that was the last one anyway
if (count > 1)
{
batchText = content.Substring(offsets[count-1], batchInfos[count - 1].length);
BatchDefinition lastBatchDef = GetLastBatchDefinition(reader, batchInfos[count - 1], batchText);
batchDefinitionList.Add(lastBatchDef);
}
}
}
return batchDefinitionList;
}
private static int GetMaxStartLine(IList<BatchInfo> batchInfos)
{
int highest = 0;
foreach (var batchInfo in batchInfos)
{
if (batchInfo.startLine > highest)
{
highest = batchInfo.startLine;
}
}
return highest;
}
/// <summary>
/// Gets offsets for all batches
/// </summary>
private static List<int> GetOffsets(string content, IList<BatchInfo> batchInfos)
{
List<int> offsets = new List<int>();
int count = 0;
int offset = 0;
bool foundAllOffsets = false;
int maxStartLine = GetMaxStartLine(batchInfos);
using (StringReader reader = new StringReader(content))
{
// go until we have found offsets for all batches
while (!foundAllOffsets)
{
// go until the last start line of the batches
for (int i = 0; i <= maxStartLine ; i++)
{
// get offset for the current batch
ReadLines(reader, ref count, ref offset, ref foundAllOffsets, batchInfos, offsets, i);
// if we found all the offsets, then we're done
if (foundAllOffsets)
{
break;
}
}
}
}
return offsets;
}
/// <summary>
/// Helper function to read lines of batches to get offsets
/// </summary>
private static void ReadLines(StringReader reader, ref int count, ref int offset, ref bool foundAllOffsets,
IList<BatchInfo> batchInfos, List<int> offsets, int iteration)
{
int ch;
while (true)
{
if (batchInfos[count].startLine == iteration)
{
count++;
offsets.Add(offset);
if (count == batchInfos.Count)
{
foundAllOffsets = true;
break;
}
}
ch = reader.Read();
if (ch == -1) // EOF do nothing
{
break;
}
else if (ch == 10 /* for \n */) // End of line increase and break
{
offset++;
break;
}
else // regular char just increase
{
offset++;
}
}
}
/// <summary>
/// Helper method to get the last batch
/// </summary>
private static BatchDefinition GetLastBatchDefinition(StringReader reader,
BatchInfo batchInfo, string batchText)
{
int startLine = batchInfo.startLine;
int startColumn = batchInfo.startColumn;
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 + 1,
batchInfo.executionCount
);
}
/// <summary>
/// Helper function to get correct lines and columns
/// in a single batch with multiple statements
/// </summary>
private static Tuple<int, int> GetBatchPositionDetails(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);
}
/// <summary>
/// Get end line and end column
/// </summary>
private static Tuple<int, int> ReadLines(StringReader reader, int n, int endLine)
{
Validate.IsNotNull(nameof(reader), reader);
int endColumn = 0;
// if only one batch with multiple lines
if (n == 0)
{
return GetBatchPositionDetails(reader, endLine);
}
// if there are more than one batch
for (int i = 0; i < n; i++)
{
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
{
++endLine;
endColumn = 0;
break;
}
else // regular char just increase
{
++endColumn;
}
}
}
return Tuple.Create(endLine, endColumn);
}
/// <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)
{
batchInfos = new List<BatchInfo>();
// 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, batchInfos);
return batchDefinitionList;
}
#region ExecutionEngine Event Handlers
private void OnBatchParserExecutionError(object sender, BatchParserExecutionErrorEventArgs args)
{
if (args != null)
{
Logger.Write(TraceEventType.Verbose, SR.BatchParserWrapperExecutionError);
throw new Exception(string.Format(CultureInfo.CurrentCulture,
SR.BatchParserWrapperExecutionEngineError, args.Message + Environment.NewLine + '\t' + args.Description));
}
}
private void OnBatchParserExecutionFinished(object sender, BatchParserExecutionFinishedEventArgs args)
{
try
{
if (args != null && args.Batch != null)
{
// 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
batchInfos.Add(new BatchInfo(args.Batch.TextSpan.iStartLine, args.Batch.TextSpan.iStartIndex, batchTextLength, args.Batch.ExpectedExecutionCount));
}
}
catch (NotImplementedException)
{
// intentionally swallow
}
catch (Exception e)
{
// adding this for debugging
Logger.Write(TraceEventType.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(TraceEventType.Information, SR.BatchParserWrapperExecutionEngineError);
throw new Exception(SR.BatchParserWrapperExecutionEngineError);
}
}
public void OnBatchMessage(object sender, BatchMessageEventArgs args)
{
#if DEBUG
if (args != null)
{
Logger.Write(TraceEventType.Information, SR.BatchParserWrapperExecutionEngineBatchMessage);
}
#endif
}
public void OnBatchResultSetProcessing(object sender, BatchResultSetEventArgs args)
{
#if DEBUG
if (args != null && args.DataReader != null)
{
Logger.Write(TraceEventType.Information, SR.BatchParserWrapperExecutionEngineBatchResultSetProcessing);
}
#endif
}
public void OnBatchResultSetFinished(object sender, EventArgs args)
{
#if DEBUG
Logger.Write(TraceEventType.Information, SR.BatchParserWrapperExecutionEngineBatchResultSetFinished);
#endif
}
public void OnBatchCancelling(object sender, EventArgs args)
{
Logger.Write(TraceEventType.Information, 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;
batchInfos = null;
}
}
}
#endregion
private class BatchInfo
{
public BatchInfo(int startLine, int startColumn, int length, int repeatCount = 1)
{
this.startLine = startLine;
this.startColumn = startColumn;
this.length = length;
this.executionCount = repeatCount;
}
public int startLine;
public int startColumn;
public int length;
public int executionCount;
}
}
}

View File

@@ -0,0 +1,19 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
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; }
}
}

View File

@@ -0,0 +1,27 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.SqlTools.ServiceLayer.BatchParser
{
public 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,
}
}

View File

@@ -0,0 +1,892 @@
//
// 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.Utility;
using Microsoft.SqlTools.ManagedBatchParser;
namespace Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
{
/// <summary>
/// Single batch of SQL command
/// </summary>
public 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 int expectedExecutionCount = 1;
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>
/// The number of times this batch is expected to be executed. Will be 1 by default, but for statements
/// with "GO 2" or other numerical values, will have a number > 1
/// </summary>
public int ExpectedExecutionCount
{
get
{
return expectedExecutionCount;
}
set
{
expectedExecutionCount = 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(TraceEventType.Error, "Expected handler to be declared");
}
if (null != connectionWrapper)
{
connectionWrapper.InfoMessage -= messageHandler;
}
if (commandWrapper != null)
{
if (statementCompletedHandler == null)
{
Logger.Write(TraceEventType.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
}
}

View File

@@ -0,0 +1,75 @@
//
// 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, int executionCount)
{
BatchText = batchText;
StartLine = startLine;
EndLine = endLine;
StartColumn = startColumn;
EndColumn = endColumn;
// set the batch execution count, with min value of 1
BatchExecutionCount = executionCount > 0 ? executionCount : 1;
}
/// <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 associated with the BatchDefinition
/// </summary>
public string BatchText
{
get; private set;
}
/// <summary>
/// Get number of times to execute this batch
/// </summary>
public int BatchExecutionCount
{
get; private set;
}
}
}

View File

@@ -0,0 +1,142 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.Data.SqlClient;
using Microsoft.SqlTools.ManagedBatchParser;
namespace Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
{
/// <summary>
/// Error totalAffectedRows for a Batch
/// </summary>
public 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
}
}

View File

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

View File

@@ -0,0 +1,161 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
namespace Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
{
/// <summary>
/// Class that parses queries into batches
/// </summary>
public 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.");
}
public void DisableVariableSubstitution()
{
variableSubstitutionDisabled = true;
}
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,134 @@
//
// 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;
using Microsoft.SqlTools.ManagedBatchParser;
namespace Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
{
/// <summary>
/// Class for handling SQL CMD by Batch Parser
/// </summary>
public 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
}
public ConnectionChangedDelegate ConnectionChanged
{
get { return connectionChangedDelegate; }
set { connectionChangedDelegate = value; }
}
public 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;
}
}
}

View File

@@ -0,0 +1,52 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.Data;
namespace Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
{
/// <summary>
/// Class associated with setting batch results
/// </summary>
public 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;
}
}
}
}

View File

@@ -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.Utility;
namespace Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
{
public 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
}
}

View File

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

View File

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

View File

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

View File

@@ -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
{
public class ScriptExecutionFinishedEventArgs : EventArgs
{
internal ScriptExecutionFinishedEventArgs(ScriptExecutionResult result)
{
ExecutionResult = result;
}
public ScriptExecutionResult ExecutionResult
{
private set;
get;
}
public bool Disposing
{
get;
set;
}
}
}

View File

@@ -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]
public enum ScriptExecutionResult
{
Success = 0x1,
Failure = 0x2,
Cancel = 0x4,
Halted = 0x8,
All = 0x0F
}
}

View File

@@ -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
{
public enum ScriptMessageType
{
FatalError,
Error,
Warning
}
}

View File

@@ -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]
public enum ShowPlanType
{
None = 0x0,
ActualExecutionShowPlan = 0x1,
ActualXmlShowPlan = 0x2,
EstimatedExecutionShowPlan = 0x4,
EstimatedXmlShowPlan = 0x8,
AllXmlShowPlan = ActualXmlShowPlan | EstimatedXmlShowPlan,
AllExecutionShowPlan = ActualExecutionShowPlan | EstimatedExecutionShowPlan,
AllShowPlan = AllExecutionShowPlan | AllXmlShowPlan
}
}

View File

@@ -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
{
public struct TextSpan
{
public int iEndIndex;
public int iEndLine;
public int iStartIndex;
public int iStartLine;
}
}

View File

@@ -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
{
public interface ICommandHandler
{
BatchParserAction Go(TextBlock batch, int repeatCount);
BatchParserAction OnError(Token token, OnErrorAction action);
BatchParserAction Include(TextBlock filename, out TextReader stream, out string newFilename);
}
}

View File

@@ -0,0 +1,13 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.SqlTools.ServiceLayer.BatchParser
{
public interface IVariableResolver
{
string GetVariable(PositionStruct pos, string name);
void SetVariable(PositionStruct pos, string name, string value);
}
}

View File

@@ -0,0 +1,748 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using Microsoft.SqlTools.ManagedBatchParser;
namespace Microsoft.SqlTools.ServiceLayer.BatchParser
{
/// <summary>
/// Lexer class for the SMO Batch Parser
/// </summary>
public 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; }
}
}

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

View File

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

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

View File

@@ -0,0 +1,13 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.SqlTools.ServiceLayer.BatchParser
{
public enum OnErrorAction
{
Ignore = 0,
Exit = 1,
}
}

View File

@@ -0,0 +1,723 @@
//
// 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;
using Microsoft.SqlTools.ManagedBatchParser;
namespace Microsoft.SqlTools.ServiceLayer.BatchParser
{
/// <summary>
/// The Parser class on which the Batch Parser is based on
/// </summary>
public 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;
}
public 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();
}
}
}
}

View File

@@ -0,0 +1,49 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
namespace Microsoft.SqlTools.ServiceLayer.BatchParser
{
[Serializable]
public 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; } }
}
}

View File

@@ -0,0 +1,67 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Collections.Generic;
using System.Text;
namespace Microsoft.SqlTools.ServiceLayer.BatchParser
{
public 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();
}
}
}

View File

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

View 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
{
public 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; }
}
}

View File

@@ -0,0 +1,44 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.SqlTools.ServiceLayer.BatchParser
{
/// <summary>
/// Class for reference of variables used by the lexer
/// </summary>
public 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; }
}
}