From 5c858ec7ff740b890ef5cb90645ec0bfe3f616e5 Mon Sep 17 00:00:00 2001 From: Isaac Devine Date: Tue, 18 Aug 2026 16:09:58 +1200 Subject: [PATCH 1/2] Add Oracle SQL*Plus compatibility tests Add unit tests to cover edges exposed by SQL*Plus handling of the Oracle specific batch separators. Notably the current regex-based statement splitter fails these tests. --- .../Basic_tests/OracleStatementSplitter_.cs | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/unittests/Oracle/Basic_tests/OracleStatementSplitter_.cs b/unittests/Oracle/Basic_tests/OracleStatementSplitter_.cs index c5534b7b..4660ee9f 100644 --- a/unittests/Oracle/Basic_tests/OracleStatementSplitter_.cs +++ b/unittests/Oracle/Basic_tests/OracleStatementSplitter_.cs @@ -242,6 +242,42 @@ public void slash_with_semicolon_directly_after() Assert.Equal(["jalla ", ";"], result); } + [Fact] + public void slash_in_a_single_quoted_literal_with_an_escaped_quote_is_not_a_separator() + { + const string sql_to_match = "select 'can''t / here' from dual"; + + var result = _splitter.Split(sql_to_match).ToList(); + + Assert.Equal([sql_to_match], result); + } + + [Theory] + [InlineData("q'[/]'", "slash in bracket quoted literal")] + [InlineData("q'{ / }'", "slash in brace quoted literal")] + [InlineData("q'< / >'", "slash in angle quoted literal")] + [InlineData("q'! / !'", "slash in delimiter quoted literal")] + public void slash_in_an_oracle_alternative_quoted_literal_is_not_a_separator( + string literal, + string _) + { + var sql_to_match = $"select {literal} from dual"; + + var result = _splitter.Split(sql_to_match).ToList(); + + Assert.Equal([sql_to_match], result); + } + + [Fact] + public void slash_only_with_crlf_is_not_a_batch() + { + const string sql_to_match = " \r\n/\r\n"; + + var result = _splitter.Split(sql_to_match).ToList(); + + Assert.Empty(result); + } + } public class should_not_replace_on @@ -482,6 +518,92 @@ public void slash_inside_of_comments_with_symbols_after_on_different_lines() } } + public class sqlplus_compatibility + { + private readonly OracleStatementSplitter _splitter = new(); + + [Fact] + public void standalone_slash_executes_a_plsql_buffer() + { + const string sql = "BEGIN\n NULL;\nEND;\n/\n"; + + var result = _splitter.Split(sql).ToList(); + + Assert.Equal(["BEGIN\n NULL;\nEND;\n"], result); + } + + [Fact] + public void semicolons_inside_a_plsql_buffer_do_not_split_the_buffer() + { + const string sql = "BEGIN\n NULL;\n NULL;\nEND;\n/\n"; + + var result = _splitter.Split(sql).ToList(); + + Assert.Single(result); + Assert.Equal(sql[..^2], result[0]); + } + + [Fact] + public void slash_in_a_division_expression_is_not_a_batch_separator() + { + const string sql = "SELECT 10 / 2 FROM dual;"; + + var result = _splitter.Split(sql).ToList(); + + Assert.Equal([sql], result); + } + + [Fact] + public void inline_slash_is_not_a_batch_separator() + { + const string sql = "SELECT 1 / 2 FROM dual; SELECT 3 FROM dual;"; + + var result = _splitter.Split(sql).ToList(); + + Assert.Equal([sql], result); + } + + [Fact] + public void blank_lines_do_not_execute_or_split_the_sqlplus_buffer() + { + const string sql = "SELECT 1 FROM dual;\n\nSELECT 2 FROM dual;"; + + var result = _splitter.Split(sql).ToList(); + + Assert.Equal([sql], result); + } + + [Fact] + public void standalone_slash_with_trailing_spaces_executes_at_eof() + { + const string sql = "BEGIN\n NULL;\nEND;\n/ "; + + var result = _splitter.Split(sql).ToList(); + + Assert.Equal(["BEGIN\n NULL;\nEND;\n"], result); + } + + [Fact] + public void repeated_standalone_slashes_do_not_create_empty_batches() + { + const string sql = "BEGIN\n NULL;\nEND;\n/\n/\n"; + + var result = _splitter.Split(sql).ToList(); + + Assert.Single(result); + } + + [Fact] + public void standalone_slash_works_with_crlf_line_endings() + { + const string sql = "BEGIN\r\n NULL;\r\nEND;\r\n/\r\n"; + + var result = _splitter.Split(sql).ToList(); + + Assert.Equal(["BEGIN\r\n NULL;\r\nEND;\r\n"], result); + } + } + public class Split { private readonly OracleStatementSplitter _splitter = new(); From ff13ffa0a55a1d799c380da9d09511209962830e Mon Sep 17 00:00:00 2001 From: Isaac Devine Date: Tue, 18 Aug 2026 16:31:57 +1200 Subject: [PATCH 2/2] Use a tokenizer for Oracle SQL batch splitting Notably this makes the batch splitting compatible with Oracle SQL*Plus. Important changes to be aware of is that it will *never* split with a / unless it is on its own line and not in a string. You can see the documentation on this here: https://docs.oracle.com/en/database/oracle/oracle-database/26/sqpug/slash.html https://docs.oracle.com/database/121/SQPUG/ch_four.htm#SQPUG395 This supercedes https://github.com/grate-devs/grate/pull/329 and helps with https://github.com/grate-devs/grate/issues/642 Building on those issues and changes it will also split batches on ; however it will retain the semicolon in the batch so that it won't cause a parser error with Oracle. Additionally this requires handling BEGIN, END and DECLARE blocks as the semicolons should be ignored in there. This does change behavior in that whitespace may be trimmed on batches, depending on where the ; or / separator was. --- .../Infrastructure/OracleStatementSplitter.cs | 200 +++++++++++++++++- .../Basic_tests/OracleStatementSplitter_.cs | 190 ++++++++++++----- .../OracleSplitterContext.cs | 15 +- 3 files changed, 337 insertions(+), 68 deletions(-) diff --git a/src/grate.oracle/Infrastructure/OracleStatementSplitter.cs b/src/grate.oracle/Infrastructure/OracleStatementSplitter.cs index b04692d6..88d110a6 100644 --- a/src/grate.oracle/Infrastructure/OracleStatementSplitter.cs +++ b/src/grate.oracle/Infrastructure/OracleStatementSplitter.cs @@ -1,11 +1,201 @@ +using System.Text.RegularExpressions; using grate.Infrastructure; namespace grate.Oracle.Infrastructure; -public class OracleStatementSplitter : RegexStatementSplitter +/// +/// Splits Oracle scripts on semicolons and SQL*Plus slash commands without treating +/// separators in comments or quoted literals as batch separators. +/// +public partial class OracleStatementSplitter : IStatementSplitter { - protected override string StringsRegex => @"(?'[^']*')"; - protected override string DashCommentsRegex => @"(?--.*$)"; - protected override string StarCommentsRegex => @"(?/\*[\S\s]*?\*/)"; - protected override string SeparatorRegex => @"(?^|\s)(?/)(?\s|;|$)"; + private enum TokenType + { + BlockStart, + DeclareBlockStart, + BlockEnd, + Semicolon, + SQLPLUSEXECUTE, + StringLiteral, + OpenCustomQuotedLiteral, + MultiLineCommentStart, + MultiLineCommentEnd, + SingleLineComment, + NewLine + } + + private readonly record struct Token( + TokenType Type, + int Index, + int Length, + string CustomQuoteDelimiter = ""); + + private const string OpenCustomQuotedLiteralPattern = + "(?\\b[qQ]'(?[^'\\s]))"; + private const string StringLiteralPattern = "(?')"; + private const string MultiLineCommentStartPattern = "(?/\\*)"; + private const string MultiLineCommentEndPattern = "(?\\*/)"; + private const string SingleLineCommentPattern = "(?--)"; + private const string BlockStartPattern = "(?\\bBEGIN\\b)"; + private const string DeclareBlockStartPattern = "(?\\bDECLARE\\b)"; + + // Note that this captures ; as well, so that we avoid capturing END IF; etc. + private const string BlockEndPattern = "(?\\bEND;)"; + private const string SemicolonPattern = "(?;)"; + private const string SqlPlusExecutePattern = "(?^[ \\t]*/[ \\t]*(?=\\r?$))"; + private const string NewLinePattern = "(?\\r\\n|\\r|\\n)"; + + [GeneratedRegex( + OpenCustomQuotedLiteralPattern + "|" + + StringLiteralPattern + "|" + + MultiLineCommentStartPattern + "|" + + MultiLineCommentEndPattern + "|" + + SingleLineCommentPattern + "|" + + BlockStartPattern + "|" + + DeclareBlockStartPattern + "|" + + BlockEndPattern + "|" + + SemicolonPattern + "|" + + SqlPlusExecutePattern + "|" + + NewLinePattern, + RegexOptions.Multiline | RegexOptions.IgnoreCase)] + private static partial Regex TokenPattern(); + + [GeneratedRegex(@"\S")] + private static partial Regex SignificantTextPattern(); + + public IEnumerable Split(string statement) => + BreakIntoBatches(statement).Where(batch => SignificantTextPattern().IsMatch(batch)); + + private static IEnumerable Tokenize(string sql) + { + for (var match = TokenPattern().Match(sql); match.Success; match = match.NextMatch()) + { + var index = match.Index; + var tokenGroup = match.Groups + .Cast() + .Single(group => group.Success && group.Name is not "0" and not "CustomQuoteDelimiter"); + var tokenType = Enum.Parse(tokenGroup.Name); + string CustomQuoteDelimiter = ""; + if (tokenType == TokenType.OpenCustomQuotedLiteral) + { + CustomQuoteDelimiter = match.Groups["CustomQuoteDelimiter"].Value; + } + yield return new Token(tokenType, index, match.Length, CustomQuoteDelimiter); + } + } + + private static IEnumerable BreakIntoBatches(string sql) + { + using var tokens = Tokenize(sql).GetEnumerator(); + var cutIndex = 0; + var blockDepth = 0; + var inDeclareBlock = false; + var token = default(Token); + + bool NextToken() + { + if (!tokens.MoveNext()) + { + return false; + } + token = tokens.Current; + return true; + } + + while(NextToken()) + { + switch (token.Type) + { + case TokenType.DeclareBlockStart: + blockDepth++; + inDeclareBlock = true; + break; + case TokenType.BlockStart: + // a declare block is eventually followed by a BEGIN + // so we need to check if we were preceded by a DECLARE + if (inDeclareBlock) + { + inDeclareBlock = false; + } else { + blockDepth++; + } + break; + + case TokenType.BlockEnd: + blockDepth--; + if (blockDepth == 0) + { + yield return sql[cutIndex..(token.Index + token.Length)]; + cutIndex = token.Index + token.Length; + } + break; + + case TokenType.StringLiteral: + while(NextToken() && token.Type != TokenType.StringLiteral) + { + + } + break; + case TokenType.OpenCustomQuotedLiteral: + var closingDelimiter = GetClosingCustomQuoteDelimiter(token.CustomQuoteDelimiter); + while(NextToken()) { + // Custom Delimiters come just before the ' string + // literal so check for that. + if (token.Type == TokenType.StringLiteral && + sql[..token.Index].EndsWith(closingDelimiter) + ) { + // end of string literal + break; + } + } + break; + + case TokenType.MultiLineCommentStart: + var depth = 1; + while (depth > 0 && NextToken()) + { + depth += token.Type switch + { + TokenType.MultiLineCommentStart => 1, + TokenType.MultiLineCommentEnd => -1, + _ => 0 + }; + } + break; + + case TokenType.SingleLineComment: + while (NextToken() && token.Type != TokenType.NewLine) + { + } + break; + + case TokenType.Semicolon: + // We want to include the semicolon in the batch + if (blockDepth > 0) + { + break; + } + + yield return sql[cutIndex..(token.Index + token.Length)]; + cutIndex = token.Index + token.Length; + break; + + case TokenType.SQLPLUSEXECUTE: + yield return sql[cutIndex..token.Index]; + cutIndex = token.Index + token.Length; + break; + } + } + + yield return sql[cutIndex..]; + } + + private static string GetClosingCustomQuoteDelimiter(string delimiter) => delimiter switch + { + "[" => "]", + "{" => "}", + "(" => ")", + "<" => ">", + _ => delimiter + }; } diff --git a/unittests/Oracle/Basic_tests/OracleStatementSplitter_.cs b/unittests/Oracle/Basic_tests/OracleStatementSplitter_.cs index 4660ee9f..aaf6d494 100644 --- a/unittests/Oracle/Basic_tests/OracleStatementSplitter_.cs +++ b/unittests/Oracle/Basic_tests/OracleStatementSplitter_.cs @@ -9,7 +9,7 @@ namespace Oracle.Basic_tests; public class OracleStatementSplitter_ { - private const string Symbols_to_check = "`~!@#$%^&*()-_+=,.;:'\"[]\\/?<>"; + private const string Symbols_to_check = "`~!@#$%^&*()-_+=,.:'\"[]\\/?<>"; private const string Words_to_check = "abcdefghijklmnopqrstuvwzyz0123456789 ABCDEFGHIJKLMNOPQRSTUVWXYZ"; // ReSharper disable once InconsistentNaming @@ -32,7 +32,7 @@ public void full_statement_without_issue() var result = _splitter.Split(sql_to_match).ToList(); Assert.NotEmpty(result); Assert.True(result.Count > 1, "Should split into multiple statements"); - Assert.Equal(result, OracleSplitterContext.FullSplitter.PLSqlStatementScrubbed); + Assert.Equal(OracleSplitterContext.FullSplitter.PLSqlStatementScrubbed, result); } [Fact] @@ -50,7 +50,7 @@ public void slash_with_tab() string sql_to_match = @" /" + "\t"; _testOutput.WriteLine(sql_to_match); var result = _splitter.Split(sql_to_match).ToList(); - Assert.Equal(["\t"],result); + Assert.Empty(result); } [Fact] @@ -84,22 +84,14 @@ public void slash_with_new_line() [Theory] [InlineData("\n", "LF")] - public void slash_with_one_new_line_after_double_dash_comments_lf(string line_ending, string _) - { - string sql_to_match = $"--{line_ending}/{line_ending}"; - _testOutput.WriteLine(sql_to_match); - var result = _splitter.Split(sql_to_match).ToList(); - Assert.Equal(["--" + line_ending], result); - } - - [Theory] [InlineData("\r\n", "CRLF")] - public void slash_with_one_new_line_after_double_dash_comments_crlf(string line_ending, string _) + + public void slash_with_one_new_line_after_double_dash_comments(string line_ending, string _) { string sql_to_match = $"--{line_ending}/{line_ending}"; _testOutput.WriteLine(sql_to_match); var result = _splitter.Split(sql_to_match).ToList(); - Assert.Equal(["--" + line_ending, line_ending], result); + Assert.Equal(["--" + line_ending], result); } [Fact] @@ -161,7 +153,7 @@ public void slash_with_symbols_and_words_before() "; _testOutput.WriteLine(sql_to_match); var result = _splitter.Split(sql_to_match).ToList(); - Assert.Single(result); + Assert.Equal([sql_to_match], result); } [Fact] @@ -192,26 +184,6 @@ public void slash_with_words_after_on_the_same_line_including_symbols() Assert.Single(result); } - [Fact] - public void slash_with_words_before_and_after_on_the_same_line() - { - string sql_to_match = Words_to_check + @" / " + Words_to_check; - _testOutput.WriteLine(sql_to_match); - var result = _splitter.Split(sql_to_match).ToList(); - Assert.Equal([Words_to_check + " "," " + Words_to_check], result); - } - - [Fact] - public void slash_with_words_before_and_after_on_the_same_line_including_symbols() - { - string sql_to_match = Words_to_check + Symbols_to_check.Replace("'", "").Replace("\"", "") + - " / BOB" + Symbols_to_check; - _testOutput.WriteLine(sql_to_match); - var result = _splitter.Split(sql_to_match).ToList(); - Assert.Equal([ Words_to_check + Symbols_to_check.Replace("'", "").Replace("\"", "") + - " ", " BOB" + Symbols_to_check], result); - } - [Fact] public void slash_after_double_dash_comment_with_single_quote_and_single_quote_after_slash() { @@ -233,15 +205,6 @@ public void slash_with_comment_after() Assert.Single(result); } - [Fact] - public void slash_with_semicolon_directly_after() - { - string sql_to_match = "jalla /;"; - _testOutput.WriteLine(sql_to_match); - var result = _splitter.Split(sql_to_match).ToList(); - Assert.Equal(["jalla ", ";"], result); - } - [Fact] public void slash_in_a_single_quoted_literal_with_an_escaped_quote_is_not_a_separator() { @@ -290,6 +253,34 @@ public should_not_replace_on(ITestOutputHelper testOutput) _testOutput = testOutput; _splitter = new OracleStatementSplitter(); } + + [Fact] + public void slash_with_semicolon_directly_after() + { + string sql_to_match = "jalla /;"; + _testOutput.WriteLine(sql_to_match); + var result = _splitter.Split(sql_to_match).ToList(); + Assert.Equal(["jalla /;"], result); + } + + [Fact] + public void slash_with_words_before_and_after_on_the_same_line() + { + string sql_to_match = Words_to_check + @" / " + Words_to_check; + _testOutput.WriteLine(sql_to_match); + var result = _splitter.Split(sql_to_match).ToList(); + Assert.Equal([sql_to_match], result); + } + + [Fact] + public void slash_with_words_before_and_after_on_the_same_line_including_symbols() + { + string sql_to_match = Words_to_check + Symbols_to_check.Replace("'", "").Replace("\"", "") + + " / BOB" + Symbols_to_check; + _testOutput.WriteLine(sql_to_match); + var result = _splitter.Split(sql_to_match).ToList(); + Assert.Equal([sql_to_match], result); + } [Fact] public void slash_when_slash_is_the_last_part_of_the_last_word_on_a_line() @@ -529,7 +520,7 @@ public void standalone_slash_executes_a_plsql_buffer() var result = _splitter.Split(sql).ToList(); - Assert.Equal(["BEGIN\n NULL;\nEND;\n"], result); + Assert.Equal(["BEGIN\n NULL;\nEND;"], result); } [Fact] @@ -540,23 +531,79 @@ public void semicolons_inside_a_plsql_buffer_do_not_split_the_buffer() var result = _splitter.Split(sql).ToList(); Assert.Single(result); - Assert.Equal(sql[..^2], result[0]); + Assert.Equal(sql[..^3], result[0]); } [Fact] - public void slash_in_a_division_expression_is_not_a_batch_separator() + public void semicolons_inside_a_declare_block_do_not_split_the_buffer() { - const string sql = "SELECT 10 / 2 FROM dual;"; + const string sql = "DECLARE\n value NUMBER := 1;\nBEGIN\n value := value + 1;\nEND;"; var result = _splitter.Split(sql).ToList(); - Assert.Equal([sql], result); + Assert.Single(result); + Assert.Equal(sql, result[0]); + } + + [Theory] + [InlineData("begin\n NULL;\nend;")] + [InlineData("BeGiN\n NULL;\nEnD;")] + public void begin_and_end_are_case_insensitive(string sql) + { + var result = _splitter.Split(sql).ToList(); + + Assert.Single(result); + Assert.Equal(sql, result[0]); + } + + [Theory] + [InlineData("declare\n value NUMBER := 1;\nbegin\n NULL;\nend;")] + [InlineData("DeClArE\n value NUMBER := 1;\nBeGiN\n NULL;\nEnD;")] + public void declare_blocks_are_case_insensitive(string sql) + { + var result = _splitter.Split(sql).ToList(); + + Assert.Single(result); + Assert.Equal(sql, result[0]); + } + + [Fact] + public void semicolons_after_a_declare_block_do_split_the_buffer() + { + const string sql = "DECLARE\n value NUMBER := 1;\nBEGIN\n value := value + 1;\nEND;\nSELECT 1;"; + + var result = _splitter.Split(sql).ToList(); + + Assert.Equal(2, result.Count); + Assert.Contains("\nSELECT 1;", result); } [Fact] - public void inline_slash_is_not_a_batch_separator() + public void end_if_semicolon_does_not_split_a_plsql_buffer() { - const string sql = "SELECT 1 / 2 FROM dual; SELECT 3 FROM dual;"; + const string sql = "BEGIN\n IF 1 = 1 THEN\n NULL;\n END IF;\nEND;\n/\n"; + + var result = _splitter.Split(sql).ToList(); + + Assert.Single(result); + Assert.Contains("END IF;", result[0]); + } + + [Fact] + public void end_loop_semicolon_does_not_split_a_plsql_buffer() + { + const string sql = "BEGIN\n LOOP\n NULL;\n EXIT;\n END LOOP;\nEND;\n/\n"; + + var result = _splitter.Split(sql).ToList(); + + Assert.Single(result); + Assert.Contains("END LOOP;", result[0]); + } + + [Fact] + public void slash_in_a_division_expression_is_not_a_batch_separator() + { + const string sql = "SELECT 10 / 2 FROM dual;"; var result = _splitter.Split(sql).ToList(); @@ -564,9 +611,9 @@ public void inline_slash_is_not_a_batch_separator() } [Fact] - public void blank_lines_do_not_execute_or_split_the_sqlplus_buffer() + public void inline_slash_is_not_a_batch_separator() { - const string sql = "SELECT 1 FROM dual;\n\nSELECT 2 FROM dual;"; + const string sql = "SELECT 1 / 2 FROM dual;"; var result = _splitter.Split(sql).ToList(); @@ -580,7 +627,7 @@ public void standalone_slash_with_trailing_spaces_executes_at_eof() var result = _splitter.Split(sql).ToList(); - Assert.Equal(["BEGIN\n NULL;\nEND;\n"], result); + Assert.Equal(["BEGIN\n NULL;\nEND;"], result); } [Fact] @@ -600,7 +647,7 @@ public void standalone_slash_works_with_crlf_line_endings() var result = _splitter.Split(sql).ToList(); - Assert.Equal(["BEGIN\r\n NULL;\r\nEND;\r\n"], result); + Assert.Equal(["BEGIN\r\n NULL;\r\nEND;"], result); } } @@ -608,6 +655,39 @@ public class Split { private readonly OracleStatementSplitter _splitter = new(); + [Fact] + public void Splits_and_removes_semicolon() + { + const string sql = "\nSELECT * FROM v$version WHERE banner LIKE 'Oracle%';\nSELECT 1\n"; + + var batches = _splitter.Split(sql).ToArray(); + + Assert.Equal(2, batches.Length); + Assert.EndsWith(";", batches[0]); + } + + [Fact] + public void Splits_and_removes_slashes_and_semicolon() + { + const string sql = "\nSELECT * FROM v$version WHERE banner LIKE 'Oracle%';\n/\nSELECT 1\n"; + + var batches = _splitter.Split(sql).ToArray(); + + Assert.Equal(2, batches.Length); + Assert.EndsWith(";", batches[0]); + } + + [Fact] + public void Splits_and_removes_indented_slashes_and_semicolon() + { + const string sql = "\n CREATE TABLE table_one (\n col NUMBER\n );\n /\n\n CREATE TABLE table_two (\n col NUMBER\n )\n"; + + var batches = _splitter.Split(sql).ToArray(); + + Assert.Equal(2, batches.Length); + Assert.EndsWith(";", batches[0]); + } + [Fact] public void Splits_and_removes_GO_statements() { diff --git a/unittests/Oracle/TestInfrastructure/OracleSplitterContext.cs b/unittests/Oracle/TestInfrastructure/OracleSplitterContext.cs index c6adf78a..64444c82 100644 --- a/unittests/Oracle/TestInfrastructure/OracleSplitterContext.cs +++ b/unittests/Oracle/TestInfrastructure/OracleSplitterContext.cs @@ -20,7 +20,7 @@ public static class FullSplitter BOB3 / ---`~!@#$%^&*()-_+=,.;:'""[]\/?<> / +--`~!@#$%^&*()-_+=,.:'""[]\/?<> / BOB5 / @@ -45,7 +45,7 @@ public static class FullSplitter BOB9 --- `~!@#$%^&*()-_+=,.;:'""[]\/?<> +-- `~!@#$%^&*()-_+=,.:'""[]\/?<> / BOB10/ @@ -109,12 +109,12 @@ INSERT [dbo].[Foo] ([Bar]) VALUES (N'/ speed racer, / speed racer, / speed racer -- / -BOB3 ", @" +BOB3 / ---`~!@#$%^&*()-_+=,.;:'""[]\/?<> / +--`~!@#$%^&*()-_+=,.:'""[]\/?<> / BOB5 - ", @" +", @" BOB6 ", @" @@ -136,7 +136,7 @@ INSERT [dbo].[Foo] ([Bar]) VALUES (N'/ speed racer, / speed racer, / speed racer BOB9 --- `~!@#$%^&*()-_+=,.;:'""[]\/?<> +-- `~!@#$%^&*()-_+=,.:'""[]\/?<> ", @" BOB10/ @@ -164,8 +164,7 @@ yeppsasd decimal(20, 6) NULL, uhuhhh datetime NULL, slsald varchar(15) NULL, uhasdf varchar(15) NULL, - daf_asdfasdf DECIMAL(20,6) NULL; -", @" + daf_asdfasdf DECIMAL(20,6) NULL;", @" EXEC @ReturnCode = msdb.dbo.sp_add_jobstep @job_id=@jobId, @step_name=N'Daily job', @step_id=1,