Support GO N syntax to execute multiple times (#551)

* Support `GO N` syntax to execute multiple times
- Plumbed through the batch execution count from the parser and used in the batch execution code path
- Functionality matches SSMS:
  - Outputs loop start/end messages that match SSMS if you're doing multi-batch execution
  - Outputs an "ignoring failure" error if an error happens during a batch
- Added tests for this
- Manually verified end to end also

* Fixing test error
This commit is contained in:
Kevin Cunnane
2017-11-22 11:33:19 -08:00
committed by GitHub
parent 42ee96f99f
commit b8e46ce65f
35 changed files with 623 additions and 367 deletions

View File

@@ -17,52 +17,49 @@ namespace Microsoft.SqlTools.ServiceLayer.BatchParser
/// </summary>
public sealed class BatchParserWrapper : IDisposable
{
private List<Tuple<int /*startLine*/, int/*startColumn*/>> startLineColumns;
private List<int /*length*/> lengths;
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,
IList<Tuple<int, int>> positions, List<int> lengths)
private static List<BatchDefinition> ConvertToBatchDefinitionList(string content, List<BatchInfo> batchInfos)
{
List<BatchDefinition> batchDefinitionList = new List<BatchDefinition>();
if (positions.Count == 0 && lengths.Count == 0)
if (batchInfos.Count == 0)
{
return batchDefinitionList;
}
List<int> offsets = GetOffsets(content, positions);
List<int> offsets = GetOffsets(content, batchInfos);
if (!string.IsNullOrEmpty(content) && (positions.Count > 0))
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 = positions[0].Item1 + 1; //positions is 0 index based
int startLine = batchInfos[0].startLine + 1; //positions is 0 index based
int endLine = startLine;
int lineDifference = startLine - 1;
int endColumn;
int offset = offsets[0];
int startColumn = positions[0].Item2;
int count = positions.Count;
string batchText = content.Substring(offset, lengths[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 = positions[1].Item1 - positions[0].Item1;
lineDifference = batchInfos[1].startLine - batchInfos[0].startLine;
}
// get endLine, endColumn for the current batch and the lineStartOffset for the next batch
var batchInfo = ReadLines(reader, lineDifference, endLine);
endLine = batchInfo.Item1;
endColumn = batchInfo.Item2;
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(
@@ -70,27 +67,28 @@ namespace Microsoft.SqlTools.ServiceLayer.BatchParser
startLine,
endLine,
startColumn + 1,
endColumn + 1
endColumn + 1,
batchInfos[0].executionCount
);
batchDefinitionList.Add(batchDef);
if (count > 1)
{
offset = offsets[1] + positions[0].Item2;
offset = offsets[1] + batchInfos[0].startColumn;
}
// Generate the rest batch definitions
for (int index = 1; index < count - 1; index++)
{
lineDifference = positions[index + 1].Item1 - positions[index].Item1;
batchInfo = ReadLines(reader, lineDifference, endLine);
endLine = batchInfo.Item1;
endColumn = batchInfo.Item2;
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, lengths[index]);
startLine = positions[index].Item1;
startColumn = positions[index].Item2;
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(
@@ -98,7 +96,8 @@ namespace Microsoft.SqlTools.ServiceLayer.BatchParser
startLine,
endLine,
startColumn + 1,
endColumn + 1
endColumn + 1,
batchInfos[index].executionCount
);
batchDefinitionList.Add(batch);
}
@@ -107,8 +106,8 @@ namespace Microsoft.SqlTools.ServiceLayer.BatchParser
if (count > 1)
{
batchText = content.Substring(offsets[count-1], lengths[count - 1]);
BatchDefinition lastBatchDef = GetLastBatchDefinition(reader, positions[count - 1], batchText);
batchText = content.Substring(offsets[count-1], batchInfos[count - 1].length);
BatchDefinition lastBatchDef = GetLastBatchDefinition(reader, batchInfos[count - 1], batchText);
batchDefinitionList.Add(lastBatchDef);
}
@@ -117,14 +116,14 @@ namespace Microsoft.SqlTools.ServiceLayer.BatchParser
return batchDefinitionList;
}
private static int GetMaxStartLine(IList<Tuple<int, int>> positions)
private static int GetMaxStartLine(IList<BatchInfo> batchInfos)
{
int highest = 0;
foreach (var position in positions)
foreach (var batchInfo in batchInfos)
{
if (position.Item1 > highest)
if (batchInfo.startLine > highest)
{
highest = position.Item1;
highest = batchInfo.startLine;
}
}
return highest;
@@ -133,14 +132,14 @@ namespace Microsoft.SqlTools.ServiceLayer.BatchParser
/// <summary>
/// Gets offsets for all batches
/// </summary>
private static List<int> GetOffsets(string content, IList<Tuple<int, int>> positions)
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(positions);
int maxStartLine = GetMaxStartLine(batchInfos);
using (StringReader reader = new StringReader(content))
{
// go until we have found offsets for all batches
@@ -150,7 +149,7 @@ namespace Microsoft.SqlTools.ServiceLayer.BatchParser
for (int i = 0; i <= maxStartLine ; i++)
{
// get offset for the current batch
ReadLines(reader, ref count, ref offset, ref foundAllOffsets, positions, offsets, i);
ReadLines(reader, ref count, ref offset, ref foundAllOffsets, batchInfos, offsets, i);
// if we found all the offsets, then we're done
if (foundAllOffsets)
@@ -168,16 +167,16 @@ namespace Microsoft.SqlTools.ServiceLayer.BatchParser
/// 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<Tuple<int, int>> positions, List<int> offsets, int iteration)
IList<BatchInfo> batchInfos, List<int> offsets, int iteration)
{
int ch;
while (true)
{
if (positions[count].Item1 == iteration)
if (batchInfos[count].startLine == iteration)
{
count++;
offsets.Add(offset);
if (count == positions.Count)
if (count == batchInfos.Count)
{
foundAllOffsets = true;
break;
@@ -205,10 +204,10 @@ namespace Microsoft.SqlTools.ServiceLayer.BatchParser
/// Helper method to get the last batch
/// </summary>
private static BatchDefinition GetLastBatchDefinition(StringReader reader,
Tuple<int, int> position, string batchText)
BatchInfo batchInfo, string batchText)
{
int startLine = position.Item1;
int startColumn = position.Item2;
int startLine = batchInfo.startLine;
int startColumn = batchInfo.startColumn;
string prevLine = null;
string line = reader.ReadLine();
int endLine = startLine;
@@ -232,7 +231,8 @@ namespace Microsoft.SqlTools.ServiceLayer.BatchParser
startLine,
endLine,
startColumn + 1,
endColumn + 1
endColumn + 1,
batchInfo.executionCount
);
}
@@ -240,7 +240,7 @@ namespace Microsoft.SqlTools.ServiceLayer.BatchParser
/// Helper function to get correct lines and columns
/// in a single batch with multiple statements
/// </summary>
private static Tuple<int, int> GetBatchDetails(StringReader reader, int endLine)
private static Tuple<int, int> GetBatchPositionDetails(StringReader reader, int endLine)
{
string prevLine = null;
string line = reader.ReadLine();
@@ -274,7 +274,7 @@ namespace Microsoft.SqlTools.ServiceLayer.BatchParser
// if only one batch with multiple lines
if (n == 0)
{
return GetBatchDetails(reader, endLine);
return GetBatchPositionDetails(reader, endLine);
}
// if there are more than one batch
@@ -327,15 +327,13 @@ namespace Microsoft.SqlTools.ServiceLayer.BatchParser
/// </summary>
public List<BatchDefinition> GetBatches(string sqlScript)
{
startLineColumns = new List<System.Tuple<int /*startLine*/, int /*startColumn*/>>();
lengths = new List<int /* length */>();
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, startLineColumns,
lengths);
List<BatchDefinition> batchDefinitionList = ConvertToBatchDefinitionList(sqlScript, batchInfos);
return batchDefinitionList;
}
@@ -360,10 +358,6 @@ namespace Microsoft.SqlTools.ServiceLayer.BatchParser
{
if (args != null && args.Batch != null)
{
Tuple<int /*startLine*/, int/*startColumn*/> position = new Tuple<int, int>(args.Batch.TextSpan.iStartLine, args.Batch.TextSpan.iStartIndex);
// PS168371
//
// There is a bug in the batch parser where it appends a '\n' to the end of the last
@@ -383,8 +377,7 @@ namespace Microsoft.SqlTools.ServiceLayer.BatchParser
}
// Add the script info
startLineColumns.Add(position);
lengths.Add(batchTextLength);
batchInfos.Add(new BatchInfo(args.Batch.TextSpan.iStartLine, args.Batch.TextSpan.iStartIndex, batchTextLength, args.Batch.ExpectedExecutionCount));
}
}
catch (NotImplementedException)
@@ -469,11 +462,26 @@ namespace Microsoft.SqlTools.ServiceLayer.BatchParser
{
executionEngine.Dispose();
executionEngine = null;
startLineColumns = null;
lengths = 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

@@ -214,6 +214,8 @@ namespace Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
//index of the batch in collection of batches
private int index = 0;
private int expectedExecutionCount = 1;
private long totalAffectedRows = 0;
private bool hasErrors;
@@ -343,6 +345,22 @@ namespace Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
}
}
/// <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

View File

@@ -13,13 +13,15 @@ namespace Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
/// <summary>
/// Constructor method for a BatchDefinition
/// </summary>
public BatchDefinition(string batchText, int startLine, int endLine, int startColumn, int endColumn)
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>
@@ -55,11 +57,19 @@ namespace Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
}
/// <summary>
/// Get batch text assocaited with the BatchDefinition
/// 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

@@ -72,7 +72,6 @@ namespace Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
{
try
{
// Parsing mode we execute only once
if (conditions.IsParseOnly)
{
numBatchExecutionTimes = 1;
@@ -81,7 +80,7 @@ namespace Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
int timesLoop = numBatchExecutionTimes;
if (numBatchExecutionTimes > 1)
{
RaiseBatchMessage(String.Format(CultureInfo.CurrentCulture, SR.EE_ExecutionInfo_InitilizingLoop, numBatchExecutionTimes));
RaiseBatchMessage(string.Format(CultureInfo.CurrentCulture, SR.EE_ExecutionInfo_InitializingLoop));
}
while (timesLoop > 0 && result != ScriptExecutionResult.Cancel && result != ScriptExecutionResult.Halted)
@@ -116,13 +115,13 @@ namespace Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
if (result == ScriptExecutionResult.Cancel)
{
RaiseBatchMessage(String.Format(CultureInfo.CurrentCulture, SR.EE_ExecutionInfo_QueryCancelledbyUser));
RaiseBatchMessage(string.Format(CultureInfo.CurrentCulture, SR.EE_ExecutionInfo_QueryCancelledbyUser));
}
else
{
if (numBatchExecutionTimes > 1)
{
RaiseBatchMessage(String.Format(CultureInfo.CurrentCulture, SR.EE_ExecutionInfo_FinalizingLoop, numBatchExecutionTimes));
RaiseBatchMessage(string.Format(CultureInfo.CurrentCulture, SR.EE_ExecutionInfo_FinalizingLoop, numBatchExecutionTimes));
}
}
}
@@ -391,6 +390,7 @@ namespace Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
currentBatch.Text = batchScript;
currentBatch.TextSpan = textSpan;
currentBatch.BatchIndex = currentBatchIndex;
currentBatch.ExpectedExecutionCount = numBatchExecutionTimes;
currentBatchIndex++;
@@ -489,7 +489,7 @@ namespace Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
result = ScriptExecutionResult.Failure;
string info = ex.Text;
RaiseScriptError(String.Format(CultureInfo.CurrentCulture, SR.EE_ScriptError_ParsingSyntax, info), ScriptMessageType.FatalError);
RaiseScriptError(string.Format(CultureInfo.CurrentCulture, SR.EE_ScriptError_ParsingSyntax, info), ScriptMessageType.FatalError);
}
}
catch (Exception ex)
@@ -730,13 +730,13 @@ namespace Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
// SET SHOWPLAN_TEXT cannot be used with other statements in the batch
preConditionBatches.Insert(0,
new Batch(
String.Format(CultureInfo.CurrentCulture, "{0} ", ExecutionEngineConditions.ShowPlanTextStatement(true)),
string.Format(CultureInfo.CurrentCulture, "{0} ", ExecutionEngineConditions.ShowPlanTextStatement(true)),
false,
executionTimeout));
postConditionBatches.Insert(0,
new Batch(
String.Format(CultureInfo.CurrentCulture, "{0} ", ExecutionEngineConditions.ShowPlanTextStatement(false)),
string.Format(CultureInfo.CurrentCulture, "{0} ", ExecutionEngineConditions.ShowPlanTextStatement(false)),
false,
executionTimeout));
}
@@ -938,7 +938,7 @@ namespace Microsoft.SqlTools.ServiceLayer.BatchParser.ExecutionEngineCode
{
if (isSqlCmdConnection)
{
RaiseBatchMessage(String.Format(CultureInfo.CurrentCulture, "Disconnection from server {0}", ReliableConnectionHelper.GetServerName(connection)));
RaiseBatchMessage(string.Format(CultureInfo.CurrentCulture, "Disconnection from server {0}", ReliableConnectionHelper.GetServerName(connection)));
CloseConnection(connection);
}
}