From 4d65afa94ca488a78a3a646a752da4eed63877e5 Mon Sep 17 00:00:00 2001 From: dazinator Date: Sun, 6 Jul 2014 22:11:21 +0100 Subject: [PATCH] Added support (and unit tests) for Batch Commands. Can parse SQL that contains many different sql commands with terminators in the same batch as opposed to just a single command. jehugaleahsa/SQLGeneration#14 --- SQLGeneration.Tests/CommandBuilderTester.cs | 27 ++++- SQLGeneration/Builders/BatchBuilder.cs | 113 ++++++++++++++++++ SQLGeneration/Builders/BuilderVisitor.cs | 8 ++ SQLGeneration/Builders/CommandOptions.cs | 6 + SQLGeneration/Builders/DeleteBuilder.cs | 16 +++ SQLGeneration/Builders/ICommand.cs | 4 + SQLGeneration/Builders/InsertBuilder.cs | 18 +++ SQLGeneration/Builders/SelectBuilder.cs | 16 +++ SQLGeneration/Builders/SelectCombiner.cs | 16 +++ SQLGeneration/Builders/UpdateBuilder.cs | 16 +++ SQLGeneration/Generators/CommandBuilder.cs | 48 +++++++- SQLGeneration/Generators/FormattingVisitor.cs | 46 ++++++- SQLGeneration/Generators/SqlGenerator.cs | 3 +- SQLGeneration/Parsing/Parser.cs | 8 +- SQLGeneration/SQLGeneration.csproj | 1 + 15 files changed, 322 insertions(+), 24 deletions(-) create mode 100644 SQLGeneration/Builders/BatchBuilder.cs diff --git a/SQLGeneration.Tests/CommandBuilderTester.cs b/SQLGeneration.Tests/CommandBuilderTester.cs index d4fbf8b..9b771b8 100644 --- a/SQLGeneration.Tests/CommandBuilderTester.cs +++ b/SQLGeneration.Tests/CommandBuilderTester.cs @@ -1038,17 +1038,36 @@ public void TestDelete_WhereClause() #region Batch /// - /// This sees whether we can reproduce multiple insert statements in a batch. + /// This sees whether we can reproduce a batch of SQL statements. + /// + [TestMethod] + public void TestBatch_MixtureOfStatementsWithSemiColons() + { + string commandText = @"INSERT INTO Table (TestCol) VALUES(';');SELECT 1 UNION ALL SELECT 1;SELECT CASE WHEN Table.Column = 'Adm;in' THEN 'Administ;rator' ELSE 'Us;er' END FROM Table"; + assertCanReproduce(commandText); + } + + /// + /// This sees whether we can reproduce multiple insert statements in a batch using a terminator. /// [TestMethod] public void TestBatch_MultipleInserts() { - string commandText = @"INSERT INTO Table VALUES(); - INSERT INTO Table VALUES()"; + string commandText = @"INSERT INTO Table VALUES();INSERT INTO Table VALUES()"; assertCanReproduce(commandText); } - #endregion + /// + /// This tests whether the terminator is persisted when we parse and reproduce the sql. + /// + [TestMethod] + public void TestBatch_TerminatorPersists() + { + string commandText = @"INSERT INTO Table VALUES();"; + assertCanReproduce(commandText); + } + + #endregion private void assertCanReproduce(string commandText, CommandBuilderOptions options = null) { diff --git a/SQLGeneration/Builders/BatchBuilder.cs b/SQLGeneration/Builders/BatchBuilder.cs new file mode 100644 index 0000000..c95230a --- /dev/null +++ b/SQLGeneration/Builders/BatchBuilder.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace SQLGeneration.Builders +{ + + /// + /// Builds a string that contains many individual commands seprated by terminators. + /// + public class BatchBuilder : ICommand + { + private readonly IList _commands; + private bool _hasTerminator = false; + + /// + /// Initializes a new instance of a BatchBuilder. + /// + /// The commands to be in the batch. + public BatchBuilder(IList commands) + { + _commands = commands; + } + + /// + /// Initializes a new instance of a BatchBuilder. + /// + public BatchBuilder() + { + _commands = new List(); + } + + /// + /// Adds the command to the batch. + /// + /// The command to add. + public void AddCommand(ICommand command) + { + if (command == null) + { + throw new ArgumentNullException("command"); + } + _commands.Add(command); + } + + /// + /// Removes the command from the batch. + /// + /// The command to remove. + /// True if the command was removed; otherwise, false. + public bool RemoveCommand(ICommand command) + { + if (command == null) + { + throw new ArgumentNullException("command"); + } + return _commands.Remove(command); + } + + /// + /// Gets the command at the specified index. + /// + /// + /// + public ICommand GetCommand(int index) + { + return _commands[index]; + } + + /// + /// Returns the commands as an IEnumerable. + /// + /// + public IEnumerable Commands() + { + return _commands.AsEnumerable(); + } + + /// + /// Returns whether there is a single command in the batch. + /// + /// + public bool IsSingleCommand() + { + return !(_commands.Count > 1); + } + + /// + /// Gets whether this command has a terminator. + /// + public bool HasTerminator + { + get + { + return _hasTerminator; + } + set + { + _hasTerminator = value; + } + } + + /// + /// Accepts a visitor. + /// + /// + public void Accept(BuilderVisitor visitor) + { + visitor.VisitBatch(this); + } + } +} diff --git a/SQLGeneration/Builders/BuilderVisitor.cs b/SQLGeneration/Builders/BuilderVisitor.cs index b36a782..40e25d8 100644 --- a/SQLGeneration/Builders/BuilderVisitor.cs +++ b/SQLGeneration/Builders/BuilderVisitor.cs @@ -45,6 +45,14 @@ protected internal virtual void VisitAliasedSource(AliasedSource aliasedSource) } + /// + /// Visits a Batch builder. + /// + /// The item to visit. + protected internal virtual void VisitBatch(BatchBuilder item) + { + } + /// /// Visits a BetweenFilter builder. /// diff --git a/SQLGeneration/Builders/CommandOptions.cs b/SQLGeneration/Builders/CommandOptions.cs index 7f95f5b..95429c3 100644 --- a/SQLGeneration/Builders/CommandOptions.cs +++ b/SQLGeneration/Builders/CommandOptions.cs @@ -17,6 +17,7 @@ public CommandOptions() VerboseDeleteStatement = true; VerboseInnerJoin = true; VerboseOuterJoin = true; + Terminator = ';'; } /// @@ -82,5 +83,10 @@ public CommandOptions Clone() /// Gets or sets whether columns should be fully qualified within a DELETE statement. /// public bool QualifyDeleteColumns { get; set; } + + /// + /// Gets or sets the terminator to be used. + /// + public char Terminator { get; set; } } } diff --git a/SQLGeneration/Builders/DeleteBuilder.cs b/SQLGeneration/Builders/DeleteBuilder.cs index f987fa1..e4ac0a2 100644 --- a/SQLGeneration/Builders/DeleteBuilder.cs +++ b/SQLGeneration/Builders/DeleteBuilder.cs @@ -11,6 +11,7 @@ public class DeleteBuilder : IFilteredCommand { private readonly AliasedSource _table; private readonly FilterGroup _where; + private bool _hasTerminator = false; /// /// Initializes a new instance of a DeleteBuilder. @@ -70,6 +71,21 @@ public bool RemoveWhere(IFilter filter) return _where.RemoveFilter(filter); } + /// + /// Gets whether this command has a terminator. + /// + public bool HasTerminator + { + get + { + return _hasTerminator; + } + set + { + _hasTerminator = value; + } + } + void IVisitableBuilder.Accept(BuilderVisitor visitor) { visitor.VisitDelete(this); diff --git a/SQLGeneration/Builders/ICommand.cs b/SQLGeneration/Builders/ICommand.cs index 9b5cd2e..c228602 100644 --- a/SQLGeneration/Builders/ICommand.cs +++ b/SQLGeneration/Builders/ICommand.cs @@ -8,5 +8,9 @@ namespace SQLGeneration.Builders /// public interface ICommand : IVisitableBuilder { + /// + /// Gets whether the command has a terminator. + /// + bool HasTerminator { get; set; } } } diff --git a/SQLGeneration/Builders/InsertBuilder.cs b/SQLGeneration/Builders/InsertBuilder.cs index 6e02f4e..9f9d73f 100644 --- a/SQLGeneration/Builders/InsertBuilder.cs +++ b/SQLGeneration/Builders/InsertBuilder.cs @@ -12,6 +12,7 @@ public class InsertBuilder : ICommand private readonly AliasedSource _table; private readonly List _columns; private readonly IValueProvider _values; + private bool _hasTerminator = false; /// /// Initializes a new instance of a InsertBuilder. @@ -85,9 +86,26 @@ public IValueProvider Values get { return _values; } } + /// + /// Gets whether this command has a terminator. + /// + public bool HasTerminator + { + get + { + return _hasTerminator; + } + set + { + _hasTerminator = value; + } + } + void IVisitableBuilder.Accept(BuilderVisitor visitor) { visitor.VisitInsert(this); } + + } } diff --git a/SQLGeneration/Builders/SelectBuilder.cs b/SQLGeneration/Builders/SelectBuilder.cs index 0ad2116..6eee0ea 100644 --- a/SQLGeneration/Builders/SelectBuilder.cs +++ b/SQLGeneration/Builders/SelectBuilder.cs @@ -17,6 +17,7 @@ public class SelectBuilder : ISelectBuilder, IFilteredCommand private readonly List _groupBy; private readonly FilterGroup _having; private readonly SourceCollection sources; + private bool _hasTerminator = false; /// /// Initializes a new instance of a SelectBuilder. @@ -371,6 +372,21 @@ bool IValueProvider.IsValueList get { return false; } } + /// + /// Gets whether this command has a terminator. + /// + public bool HasTerminator + { + get + { + return _hasTerminator; + } + set + { + _hasTerminator = value; + } + } + void IVisitableBuilder.Accept(BuilderVisitor visitor) { visitor.VisitSelect(this); diff --git a/SQLGeneration/Builders/SelectCombiner.cs b/SQLGeneration/Builders/SelectCombiner.cs index c0bbd20..6119ee3 100644 --- a/SQLGeneration/Builders/SelectCombiner.cs +++ b/SQLGeneration/Builders/SelectCombiner.cs @@ -12,6 +12,7 @@ public abstract class SelectCombiner : ISelectBuilder private readonly ISelectBuilder leftHand; private readonly ISelectBuilder rightHand; private readonly List orderBy; + private bool _hasTerminator = false; /// /// Initializes a new instance of a SelectCombiner. @@ -116,6 +117,21 @@ bool IValueProvider.IsValueList get { return false; } } + /// + /// Gets whether this command has a terminator. + /// + public bool HasTerminator + { + get + { + return _hasTerminator; + } + set + { + _hasTerminator = value; + } + } + void IVisitableBuilder.Accept(BuilderVisitor visitor) { OnAccept(visitor); diff --git a/SQLGeneration/Builders/UpdateBuilder.cs b/SQLGeneration/Builders/UpdateBuilder.cs index 0c86457..28a9501 100644 --- a/SQLGeneration/Builders/UpdateBuilder.cs +++ b/SQLGeneration/Builders/UpdateBuilder.cs @@ -13,6 +13,7 @@ public class UpdateBuilder : IFilteredCommand private readonly AliasedSource _table; private readonly IList _setters; private readonly FilterGroup _where; + private bool _hasTerminator = false; /// /// Initializes a new instance of a UpdateBuilder. @@ -108,6 +109,21 @@ public bool RemoveWhere(IFilter filter) return _where.RemoveFilter(filter); } + /// + /// Gets whether this command has a terminator. + /// + public bool HasTerminator + { + get + { + return _hasTerminator; + } + set + { + _hasTerminator = value; + } + } + void IVisitableBuilder.Accept(BuilderVisitor visitor) { visitor.VisitUpdate(this); diff --git a/SQLGeneration/Generators/CommandBuilder.cs b/SQLGeneration/Generators/CommandBuilder.cs index 9c6cf67..9c446f7 100644 --- a/SQLGeneration/Generators/CommandBuilder.cs +++ b/SQLGeneration/Generators/CommandBuilder.cs @@ -44,8 +44,36 @@ public ICommand GetCommand(string commandText, CommandBuilderOptions options = n this.scope = new SourceScope(); this.options = options ?? new CommandBuilderOptions(); ITokenSource tokenSource = Grammar.TokenRegistry.CreateTokenSource(commandText); - MatchResult result = GetResult(tokenSource); - return buildStart(result); + + var batch = new BatchBuilder(); + + while (true) + { + MatchResult result = GetResult(tokenSource); + if (result != null && result.IsMatch) + { + var command = buildStart(result); + batch.AddCommand(command); + } + else + { + break; + } + } + + if (batch.IsSingleCommand()) + { + return batch.GetCommand(0); + } + + return batch; + } + + private void buildTerminator(MatchResult result, ICommand builder) + { + MatchResult terminator = result.Matches[SqlGrammar.Start.Terminator]; + builder.HasTerminator = terminator.IsMatch; + return; } private ICommand buildStart(MatchResult result) @@ -53,22 +81,30 @@ private ICommand buildStart(MatchResult result) MatchResult select = result.Matches[SqlGrammar.Start.SelectStatement]; if (select.IsMatch) { - return buildSelectStatement(select); + ICommand command = buildSelectStatement(select); + buildTerminator(result, command); + return command; } MatchResult insert = result.Matches[SqlGrammar.Start.InsertStatement]; if (insert.IsMatch) { - return buildInsertStatement(insert); + ICommand command = buildInsertStatement(insert); + buildTerminator(result, command); + return command; } MatchResult update = result.Matches[SqlGrammar.Start.UpdateStatement]; if (update.IsMatch) { - return buildUpdateStatement(update); + ICommand command = buildUpdateStatement(update); + buildTerminator(result, command); + return command; } MatchResult delete = result.Matches[SqlGrammar.Start.DeleteStatement]; if (delete.IsMatch) { - return buildDeleteStatement(delete); + ICommand command = buildDeleteStatement(delete); + buildTerminator(result, command); + return command; } throw new InvalidOperationException(); } diff --git a/SQLGeneration/Generators/FormattingVisitor.cs b/SQLGeneration/Generators/FormattingVisitor.cs index c2e045c..420cdad 100644 --- a/SQLGeneration/Generators/FormattingVisitor.cs +++ b/SQLGeneration/Generators/FormattingVisitor.cs @@ -40,10 +40,10 @@ public FormattingVisitor(TextWriter writer, CommandOptions options = null) } private FormattingVisitor( - TextWriter writer, - CommandOptions options, - int level, - CommandType commandType, + TextWriter writer, + CommandOptions options, + int level, + CommandType commandType, SourceReferenceType sourceType, ValueReferenceType projectionType) { @@ -107,6 +107,19 @@ protected internal override void VisitAllColumns(AllColumns item) writer.Write("*"); } + /// + /// Generates the text for a Batch builder. + /// + /// The BatchBuilder to generate the text for. + protected internal override void VisitBatch(BatchBuilder item) + { + foreach (var command in item.Commands()) + { + command.Accept(this); + } + base.VisitBatch(item); + } + /// /// Generates the text for a BetweenFilter builder. /// @@ -228,6 +241,10 @@ private void visitDelete(DeleteBuilder item) IFilter filterGroup = item.WhereFilterGroup; filterGroup.Accept(forSubCommand().forValueContext(ValueReferenceType.Reference)); } + if (item.HasTerminator) + { + writer.Write(options.Terminator); + } } /// @@ -507,6 +524,10 @@ private void visitInsert(InsertBuilder item) writer.Write("VALUES"); } item.Values.Accept(forSubCommand().forValueContext(ValueReferenceType.Reference)); + if (item.HasTerminator) + { + writer.Write(options.Terminator); + } } /// @@ -834,7 +855,7 @@ protected internal override void VisitPrecedingUnboundFrame(PrecedingUnboundFram visitUnboundFrame(item); writer.Write(" PRECEDING"); } - + /// /// Generates the text for a RightOuterJoin builder. /// @@ -911,6 +932,10 @@ private void visitSelect(SelectBuilder item) { writer.Write(")"); } + if (item.HasTerminator) + { + writer.Write(options.Terminator); + } } /// @@ -1010,6 +1035,10 @@ private void visitUpdate(UpdateBuilder item) IVisitableBuilder where = item.WhereFilterGroup; where.Accept(forSubCommand().forValueContext(ValueReferenceType.Reference)); } + if (item.HasTerminator) + { + writer.Write(options.Terminator); + } } /// @@ -1214,6 +1243,10 @@ private void visitSelectCombiner(SelectCombiner combiner, string combinerToken) { writer.Write(")"); } + if (combiner.HasTerminator) + { + writer.Write(options.Terminator); + } } private void visitBoundFrame(BoundFrame item) @@ -1312,7 +1345,8 @@ private enum CommandType Select, Insert, Update, - Delete + Delete, + Batch } private enum SourceReferenceType diff --git a/SQLGeneration/Generators/SqlGenerator.cs b/SQLGeneration/Generators/SqlGenerator.cs index b5aa2c3..0b5f876 100644 --- a/SQLGeneration/Generators/SqlGenerator.cs +++ b/SQLGeneration/Generators/SqlGenerator.cs @@ -39,7 +39,8 @@ protected SqlGrammar Grammar protected MatchResult GetResult(ITokenSource tokenSource) { Parser parser = new Parser(grammar); - return parser.Parse(SqlGrammar.Start.Name, tokenSource); + var matchedStatement = parser.Parse(SqlGrammar.Start.Name, tokenSource); + return matchedStatement; } } } diff --git a/SQLGeneration/Parsing/Parser.cs b/SQLGeneration/Parsing/Parser.cs index ee2eccb..0a9a5ca 100644 --- a/SQLGeneration/Parsing/Parser.cs +++ b/SQLGeneration/Parsing/Parser.cs @@ -39,13 +39,7 @@ public MatchResult Parse(string expressionType, ITokenSource tokenSource) Expression expression = grammar.Expression(expressionType); ParseAttempt attempt = new ParseAttempt(this, tokenSource); - MatchResult result = expression.Match(attempt, String.Empty); - - // check that there are no trailing tokens - if (result.IsMatch && attempt.GetToken() != null) - { - result.IsMatch = false; - } + MatchResult result = expression.Match(attempt, String.Empty); return result; } diff --git a/SQLGeneration/SQLGeneration.csproj b/SQLGeneration/SQLGeneration.csproj index 73c4a5a..8b4d162 100644 --- a/SQLGeneration/SQLGeneration.csproj +++ b/SQLGeneration/SQLGeneration.csproj @@ -46,6 +46,7 @@ +