/// <summary> /// Adds input batch context data to the exception data. /// </summary> /// <param name="ex">The ex.</param> /// <param name="batch">The batch.</param> /// <exception cref="ArgumentNullException">batch is null.</exception> public static void AddContextData(this SqlException ex, SqlBatch batch) { if (batch == null) { throw new ArgumentNullException(nameof(batch)); } var primaryException = ex.Errors[0]; var isProcedureError = !string.IsNullOrEmpty(primaryException.Procedure); ex.Data.Add(Server, string.IsNullOrEmpty(ex.Server) ? "Unknown" : ex.Server); ex.Data.Add(BatchBeginLineNumber, batch.BatchBeginLineNumber); ex.Data.Add(BatchSource, batch.Source); ex.Data.Add(IsProcedureError, isProcedureError); ex.Data.Add(SourceErrorLine, primaryException.LineNumber + batch.BatchBeginLineNumber - 1); if (isProcedureError) { ex.Data.Add(ErrorStatement, $"EXEC {ex.Procedure} ... (in input batch)"); } else { var sb = new StringBuilder(); var lines = batch.Sql.Split(new[] { Environment.NewLine }, StringSplitOptions.None); for (var i = Math.Max(0, ex.LineNumber - 1); i < Math.Min(lines.Length, ex.LineNumber + 10); ++i) { sb.AppendLine(lines[i]); } ex.Data.Add(ErrorStatement, sb.ToString()); } // This entry just to signify that context data was successfully added. ex.Data.Add(HasContextData, true); }
/// <summary> /// Parses this instance. /// </summary> /// <exception cref="ParserException">Syntax error or unrecognized command directive /// or /// or</exception> public void Parse() { var sw = new Stopwatch(); sw.Start(); try { if (this.runConfiguration.OutputFile != null) { this.CommandExecuter.Out(OutputDestination.File, this.runConfiguration.OutputFile); } this.InputSourceChanged += this.CommandExecuter.OnInputSourceChanged; this.CommandExecuter.ConnectWithConnectionString(this.runConfiguration.ConnectionString); this.SetInputSource(this.runConfiguration.InitialBatchSource); var tokenizer = new Tokenizer(); while (this.sourceStack.Count > 0) { this.Source = this.sourceStack.Peek(); if (this.currentBatch == null) { this.currentBatch = new SqlBatch(); } var line = this.Source.GetNextLine(); if (line == null) { // End of input source if (tokenizer.State != TokenizerState.None) { // Incomplete parse throw new ParserException(tokenizer.State, this.Source); } this.sourceStack.Pop(); continue; } tokenizer.AddLine(line); Token token; var isStartOfLine = true; while ((token = tokenizer.GetNextToken()) != null) { if (tokenizer.State == TokenizerState.None) { // Not in a comment or a multi-line string literal if (isStartOfLine) { // Command matching only when we are at the beginning of a line var commandMatched = false; foreach (var commandMatcher in this.commandMatchers) { if (!commandMatcher.IsMatch(token.TokenValue)) { continue; } switch (commandMatcher) { case GoCommand go: if (string.IsNullOrWhiteSpace(this.currentBatch.Sql)) { break; } try { // Increment first, so a failed batch is counted ++this.BatchCount; this.CommandExecuter.ProcessBatch(this.currentBatch, go.ExecutionCount); } finally { this.previousBatch = this.currentBatch; this.currentBatch = new SqlBatch(); } break; case SetvarCommand setvar: if (setvar.VarValue == null) { this.VariableResolver.DeleteVariable(setvar.VarValue); } else { this.VariableResolver.SetVariable(setvar.VarName, setvar.VarValue); } break; case IncludeCommand include: string resolvedPath; if (Path.IsPathRooted(include.Filename)) { resolvedPath = include.Filename; } else if (!include.Filename.Contains(Path.DirectorySeparatorChar)) { // Assume the included file is in the same location as the current batch source, or current directory if no source. resolvedPath = Path.Combine( File.Exists(this.Source.Filename) ? Path.GetDirectoryName(this.Source.Filename) : this.currentDirectoryResolver.GetCurrentDirectory(), include.Filename); } else { resolvedPath = include.Filename; } this.SetInputSource(this.CommandExecuter.IncludeFileName(resolvedPath)); break; case ConnectCommand connect: connect.ResolveConnectionParameters(this.VariableResolver); if (connect.Server != null) { // Execute whatever we have so far on the current connection before reconnecting try { this.CommandExecuter.ProcessBatch(this.currentBatch, 1); } finally { this.previousBatch = this.currentBatch; this.currentBatch = new SqlBatch(); ++this.BatchCount; } this.CommandExecuter.Connect( connect.Timeout, connect.Server, connect.Username, connect.Password); } break; case EdCommand _: if (!this.disableInteractiveCommands && Environment.UserInteractive) { var currentBatchStr = this.currentBatch.Sql; var batchToEdit = string.IsNullOrEmpty(currentBatchStr) ? this.previousBatch.Sql : currentBatchStr; if (!string.IsNullOrWhiteSpace(batchToEdit)) { var editedBatch = this.CommandExecuter.Ed(batchToEdit); if (editedBatch != null) { this.SetInputSource(editedBatch); this.currentBatch = new SqlBatch(); } } } break; case ExitCommand exit: if (exit.ExitImmediately) { return; } this.CommandExecuter.Exit(this.currentBatch, exit.ExitBatch); return; case ErrorCommand error: this.CommandExecuter.Error( error.OutputDestination, FileParameterCommand.GetNodeFilepath(this.nodeNumber, error.Filename)); break; case OutCommand @out: this.CommandExecuter.Out( @out.OutputDestination, FileParameterCommand.GetNodeFilepath(this.nodeNumber, @out.Filename)); break; // ReSharper disable once UnusedVariable case ServerListCommand serverList: if (!this.disableInteractiveCommands) { this.CommandExecuter.ServerList(); } break; // ReSharper disable once UnusedVariable case HelpCommand help: if (!this.disableInteractiveCommands) { this.CommandExecuter.Help(); } break; case ResetCommand _: if (!this.disableInteractiveCommands) { this.CommandExecuter.Reset(); this.currentBatch.Clear(); } break; case ListCommand _: if (!this.disableInteractiveCommands) { this.CommandExecuter.List(this.currentBatch.Sql); } break; case ListVarCommand _: if (!this.disableInteractiveCommands) { this.CommandExecuter.ListVar(this.VariableResolver.Variables); } break; case ShellCommand shell: this.CommandExecuter.ExecuteShellCommand(shell.Command); break; case QuitCommand _: this.CommandExecuter.Quit(); // Stop parsing return; case OnErrorCommand onError: this.CommandExecuter.OnError(onError.ErrorAction); break; case InvalidCommand _: throw new ParserException( "Syntax error or unrecognized command directive", this.Source); } commandMatched = true; break; } if (commandMatched) { // Jump to next line, appending a blank line where we removed the command break; } } this.AppendToken(token); } else { // In a comment, just append to batch buffer this.AppendToken(token); } isStartOfLine = false; } } if (tokenizer.State != TokenizerState.None) { // Incomplete parse throw ParserException.CreateInvalidTokenizerStateException(tokenizer.State, this.Source); } // If we have anything left, it's the last batch if (!string.IsNullOrWhiteSpace(this.currentBatch.Sql)) { // Increment first, so a failed batch is counted ++this.BatchCount; this.CommandExecuter.ProcessBatch(this.currentBatch, 1); } } catch (ParserException) { throw; } catch (SqlException) { throw; } catch (Exception ex) { throw new ParserException(ex.Message, ex, this.Source); } finally { // Output statistics sw.Stop(); var batches = this.BatchCount == 1 ? "batch" : "batches"; var errors = this.CommandExecuter.ErrorCount == 1 ? "error" : "errors"; this.CommandExecuter.WriteStdoutMessage( string.Join( Environment.NewLine, Environment.NewLine, $"{this.BatchCount} {batches} processed in {sw.Elapsed.Minutes} min, {sw.Elapsed.Seconds}.{sw.Elapsed.Milliseconds:D3} sec.", $"{this.CommandExecuter.ErrorCount} SQL {errors} in execution.")); } }