//
// 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.BatchParser.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
{
///
/// Wraps the SMO Batch parser to make it a easily useable component.
///
public sealed class BatchParserWrapper : IDisposable
{
private List batchInfos;
private ExecutionEngine executionEngine;
private BatchEventNotificationHandler notificationHandler;
///
/// Helper method used to Convert line/column information in a file to offset
///
private static List ConvertToBatchDefinitionList(string content, List batchInfos)
{
List batchDefinitionList = new List();
if (batchInfos.Count == 0)
{
return batchDefinitionList;
}
List 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 = batchInfos[0].batchText;
// 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,
batchInfos[0].sqlCmdCommand
);
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 = batchInfos[index].batchText;
startLine = batchInfos[index].startLine + 1; //positions is 0 index based
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,
batchInfos[index].sqlCmdCommand
);
batchDefinitionList.Add(batch);
}
// if there is only one batch then that was the last one anyway
if (count > 1)
{
batchText = batchInfos[count - 1].batchText;
BatchDefinition lastBatchDef = GetLastBatchDefinition(reader, batchInfos[count - 1], batchText);
batchDefinitionList.Add(lastBatchDef);
}
}
}
return batchDefinitionList;
}
private static int GetMaxStartLine(IList batchInfos)
{
int highest = 0;
foreach (var batchInfo in batchInfos)
{
if (batchInfo.startLine > highest)
{
highest = batchInfo.startLine;
}
}
return highest;
}
///
/// Gets offsets for all batches
///
private static List GetOffsets(string content, IList batchInfos)
{
List offsets = new List();
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;
}
///
/// Helper function to read lines of batches to get offsets
///
private static void ReadLines(StringReader reader, ref int count, ref int offset, ref bool foundAllOffsets,
IList batchInfos, List 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++;
}
}
}
///
/// Helper method to get the last batch
///
private static BatchDefinition GetLastBatchDefinition(StringReader reader,
BatchInfo batchInfo, string batchText)
{
int startLine = batchInfo.startLine + 1;
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,
batchInfo.sqlCmdCommand
);
}
///
/// Helper function to get correct lines and columns
/// in a single batch with multiple statements
///
private static Tuple 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);
}
///
/// Get end line and end column
///
private static Tuple 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);
}
///
/// Wrapper API for the Batch Parser that returns a list of
/// BatchDefinitions when given a string to parse
///
public BatchParserWrapper()
{
executionEngine = new ExecutionEngine();
// subscribe to executionEngine BatchParser events
executionEngine.BatchParserExecutionError += OnBatchParserExecutionError;
executionEngine.BatchParserExecutionFinished += OnBatchParserExecutionFinished;
// instantiate notificationHandler class
notificationHandler = new BatchEventNotificationHandler();
}
///
/// Takes in a query string and returns a list of BatchDefinitions
///
public List GetBatches(string sqlScript, ExecutionEngineConditions conditions = null)
{
batchInfos = new List();
// execute the script - all communication / integration after here happen via event handlers
executionEngine.ParseScript(sqlScript, notificationHandler, conditions);
// retrieve a list of BatchDefinitions
List 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, batchText, args.SqlCmdCommand, 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
///
/// Internal implementation class to implement IBatchEventHandlers
///
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, string batchText, SqlCmdCommand sqlCmdCommand, int repeatCount = 1)
{
this.startLine = startLine;
this.startColumn = startColumn;
this.executionCount = repeatCount;
this.batchText = batchText;
this.sqlCmdCommand = sqlCmdCommand;
}
public int startLine;
public int startColumn;
public int executionCount;
public string batchText;
public SqlCmdCommand sqlCmdCommand;
}
}
}