diff --git a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs index c1d3e2b8a46..20f6721c791 100644 --- a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs +++ b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs @@ -1388,6 +1388,15 @@ protected override void Generate( builder.EndCommand(); } + private enum ParsingState + { + Normal, + InBlockComment, + InSquareBrackets, + InDoubleQuotes, + InQuotes + } + /// /// Builds commands for the given by making calls on the given /// , and then terminates the final command. @@ -1402,12 +1411,13 @@ protected override void Generate(SqlOperation operation, IModel? model, Migratio .Replace("\\\r\n", "") .Split(["\r\n", "\n"], StringSplitOptions.None); - var quoted = false; + var state = ParsingState.Normal; var batchBuilder = new StringBuilder(); foreach (var line in preBatched) { var trimmed = line.TrimStart(); - if (!quoted + + if (state == ParsingState.Normal && trimmed.StartsWith("GO", StringComparison.OrdinalIgnoreCase) && (trimmed.Length == 2 || char.IsWhiteSpace(trimmed[2]))) @@ -1427,31 +1437,34 @@ protected override void Generate(SqlOperation operation, IModel? model, Migratio } else { - var commentStart = false; - foreach (var c in trimmed) + for (var i = 0; i < trimmed.Length; i++) { - switch (c) + var c = trimmed[i]; + var next = i + 1 < trimmed.Length ? trimmed[i + 1] : '\0'; + + if (state == ParsingState.Normal && c == '-' && next == '-') { - case '\'': - quoted = !quoted; - commentStart = false; - break; - case '-': - if (!quoted) - { - if (commentStart) - { - goto LineEnd; - } + goto LineEnd; + } - commentStart = true; - } + state = state switch + { + ParsingState.Normal when c == '\'' => ParsingState.InQuotes, + ParsingState.Normal when c == '[' => ParsingState.InSquareBrackets, + ParsingState.Normal when c == '"' => ParsingState.InDoubleQuotes, + ParsingState.Normal when c == '/' && next == '*' => ConsumeAndReturn(ref i, ParsingState.InBlockComment), - break; - default: - commentStart = false; - break; - } + ParsingState.InQuotes when c == '\'' => ParsingState.Normal, + + ParsingState.InSquareBrackets when c == ']' && next == ']' => ConsumeAndReturn(ref i, ParsingState.InSquareBrackets), + ParsingState.InSquareBrackets when c == ']' => ParsingState.Normal, + + ParsingState.InDoubleQuotes when c == '"' => ParsingState.Normal, + + ParsingState.InBlockComment when c == '*' && next == '/' => ConsumeAndReturn(ref i, ParsingState.Normal), + + _ => state + }; } LineEnd: @@ -1461,6 +1474,12 @@ protected override void Generate(SqlOperation operation, IModel? model, Migratio AppendBatch(batchBuilder.ToString()); + ParsingState ConsumeAndReturn(ref int index, ParsingState newState) + { + index++; + return newState; + } + void AppendBatch(string batch) { if (!string.IsNullOrWhiteSpace(batch)) diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/SqlServerMigrationsSqlGeneratorTest.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/SqlServerMigrationsSqlGeneratorTest.cs index 22ddb64c6bf..effa2a16655 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/SqlServerMigrationsSqlGeneratorTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/SqlServerMigrationsSqlGeneratorTest.cs @@ -800,6 +800,517 @@ public override void SqlOperation() """); } + [ConditionalFact] + public virtual void SqlOperation_handles_block_comment_with_single_quote() + { + Generate( + new SqlOperation { Sql = "/* It's a comment */" + EOL + "SELECT 1;" + EOL + "go" + EOL + "SELECT 2;" }); + + AssertSql( + """ +/* It's a comment */ +SELECT 1; +GO + +SELECT 2; +"""); + } + + [ConditionalFact] + public virtual void SqlOperation_handles_multiline_block_comment_with_single_quote() + { + Generate( + new SqlOperation + { + Sql = "/* This is" + EOL + " a multiline comment with ' quote */" + EOL + "SELECT 1;" + EOL + "go" + EOL + "SELECT 2;" + }); + + AssertSql( + """ +/* This is + a multiline comment with ' quote */ +SELECT 1; +GO + +SELECT 2; +"""); + } + + [ConditionalFact] + public virtual void SqlOperation_handles_block_comment_with_multiple_quotes() + { + Generate( + new SqlOperation + { + Sql = "/* It's a comment with 'multiple' quotes */" + EOL + "SELECT 1;" + EOL + "go" + EOL + "SELECT 2;" + }); + + AssertSql( + """ +/* It's a comment with 'multiple' quotes */ +SELECT 1; +GO + +SELECT 2; +"""); + } + + [ConditionalFact] + public virtual void SqlOperation_handles_block_comment_before_procedure() + { + Generate( + new SqlOperation + { + Sql = "/* It's a procedure */" + EOL + "CREATE PROCEDURE dbo.proc1 AS SELECT 1;" + EOL + "go" + EOL + + "/* Another one */" + EOL + "CREATE PROCEDURE dbo.proc2 AS SELECT 2;" + }); + + AssertSql( + """ +/* It's a procedure */ +CREATE PROCEDURE dbo.proc1 AS SELECT 1; +GO + +/* Another one */ +CREATE PROCEDURE dbo.proc2 AS SELECT 2; +"""); + } + + [ConditionalFact] + public virtual void SqlOperation_handles_empty_block_comment() + { + Generate( + new SqlOperation { Sql = "/**/" + EOL + "SELECT 1;" + EOL + "go" + EOL + "SELECT 2;" }); + + AssertSql( + """ +/**/ +SELECT 1; +GO + +SELECT 2; +"""); + } + + [ConditionalFact] + public virtual void SqlOperation_handles_nested_comments_and_strings() + { + Generate( + new SqlOperation + { + Sql = "/* Block comment */" + EOL + "SELECT 'string with '' escaped quotes';" + EOL + "go" + EOL + + "-- Line comment" + EOL + "SELECT 1;" + }); + + AssertSql( + """ +/* Block comment */ +SELECT 'string with '' escaped quotes'; +GO + +-- Line comment +SELECT 1; +"""); + } + + [ConditionalFact] + public virtual void SqlOperation_handles_block_comment_after_string() + { + Generate( + new SqlOperation { Sql = "SELECT 'test';" + EOL + "/* It's a comment */" + EOL + "go" + EOL + "SELECT 2;" }); + + AssertSql( + """ +SELECT 'test'; +/* It's a comment */ +GO + +SELECT 2; +"""); + } + + [ConditionalFact] + public virtual void SqlOperation_handles_block_comment_with_asterisks() + { + Generate( + new SqlOperation + { + Sql = "/** It's a comment with extra stars **/" + EOL + "SELECT 1;" + EOL + "go" + EOL + "SELECT 2;" + }); + + AssertSql( + """ +/** It's a comment with extra stars **/ +SELECT 1; +GO + +SELECT 2; +"""); + } + + [ConditionalFact] + public virtual void SqlOperation_handles_line_comment_with_block_comment_start() + { + Generate( + new SqlOperation { Sql = "-- /*" + EOL + "SELECT 1;" + EOL + "go" + EOL + "SELECT 2;" }); + + AssertSql( + """ +-- /* +SELECT 1; +GO + +SELECT 2; +"""); + } + + [ConditionalFact] + public virtual void SqlOperation_handles_block_comment_with_line_comment_inside() + { + Generate( + new SqlOperation { Sql = "/* Block comment -- with line comment */" + EOL + "SELECT 1;" + EOL + "go" + EOL + "SELECT 2;" }); + + AssertSql( + """ +/* Block comment -- with line comment */ +SELECT 1; +GO + +SELECT 2; +"""); + } + + [ConditionalFact] + public virtual void SqlOperation_handles_multiline_block_comment_with_go_on_separate_line() + { + Generate( + new SqlOperation + { + Sql = "/* Comment with" + EOL + "go" + EOL + "inside */" + EOL + "SELECT 1;" + EOL + "go" + EOL + "SELECT 2;" + }); + + AssertSql( + """ +/* Comment with +go +inside */ +SELECT 1; +GO + +SELECT 2; +"""); + } + + [ConditionalFact] + public virtual void SqlOperation_handles_square_bracket_identifier() + { + Generate( + new SqlOperation { Sql = "INSERT INTO [go] VALUES (1);" + EOL + "go" + EOL + "SELECT 2;" }); + + AssertSql( + """ +INSERT INTO [go] VALUES (1); +GO + +SELECT 2; +"""); + } + + [ConditionalFact] + public virtual void SqlOperation_handles_escaped_square_bracket_identifier() + { + Generate( + new SqlOperation { Sql = "INSERT INTO [g]]o] VALUES (1);" + EOL + "go" + EOL + "SELECT 2;" }); + + AssertSql( + """ +INSERT INTO [g]]o] VALUES (1); +GO + +SELECT 2; +"""); + } + + [ConditionalFact] + public virtual void SqlOperation_handles_double_quote_identifier() + { + Generate( + new SqlOperation { Sql = "INSERT INTO \"" + EOL + "go" + EOL + "\" VALUES (1);" + EOL + "go" + EOL + "SELECT 2;" }); + + AssertSql( + """ +INSERT INTO " +go +" VALUES (1); +GO + +SELECT 2; +"""); + } + + [ConditionalFact] + public virtual void SqlOperation_handles_quote_before_go() + { + Generate( + new SqlOperation { Sql = "SELECT 'test\"';" + EOL + "go" + EOL + "SELECT 2;" }); + + AssertSql( + """ +SELECT 'test"'; +GO + +SELECT 2; +"""); + } + + [ConditionalFact] + public virtual void SqlOperation_handles_forward_slash_before_go() + { + Generate( + new SqlOperation { Sql = "SELECT 1/2;" + EOL + "go" + EOL + "SELECT 2;" }); + + AssertSql( + """ +SELECT 1/2; +GO + +SELECT 2; +"""); + } + + [ConditionalFact] + public virtual void SqlOperation_handles_asterisk_before_go() + { + Generate( + new SqlOperation { Sql = "SELECT 1*2;" + EOL + "go" + EOL + "SELECT 2;" }); + + AssertSql( + """ +SELECT 1*2; +GO + +SELECT 2; +"""); + } + + [ConditionalFact] + public virtual void SqlOperation_handles_dash_before_go() + { + Generate( + new SqlOperation { Sql = "SELECT 1-2;" + EOL + "go" + EOL + "SELECT 2;" }); + + AssertSql( + """ +SELECT 1-2; +GO + +SELECT 2; +"""); + } + + [ConditionalFact] + public virtual void SqlOperation_handles_square_bracket_before_go() + { + Generate( + new SqlOperation { Sql = "SELECT [" + EOL + "column" + EOL + "];" + EOL + "go" + EOL + "SELECT 2;" }); + + AssertSql( + """ +SELECT [ +column +]; +GO + +SELECT 2; +"""); + } + + [ConditionalFact] + public virtual void SqlOperation_handles_double_quote_before_go() + { + Generate( + new SqlOperation { Sql = "SELECT \"go\"\"lum\";" + EOL + "go" + EOL + "SELECT 2;" }); + + AssertSql( + """ +SELECT "go""lum"; +GO + +SELECT 2; +"""); + } + + [ConditionalFact] + public virtual void SqlOperation_handles_line_comment_inside_quotes() + { + Generate( + new SqlOperation { Sql = "SELECT '-- not a comment';" + EOL + "go" + EOL + "SELECT 2;" }); + + AssertSql( + """ +SELECT '-- not a comment'; +GO + +SELECT 2; +"""); + } + + [ConditionalFact] + public virtual void SqlOperation_handles_line_comment_inside_square_brackets() + { + Generate( + new SqlOperation { Sql = "SELECT [-- column];" + EOL + "go" + EOL + "SELECT 2;" }); + + AssertSql( + """ +SELECT [-- column]; +GO + +SELECT 2; +"""); + } + + [ConditionalFact] + public virtual void SqlOperation_handles_line_comment_inside_double_quotes() + { + Generate( + new SqlOperation { Sql = "SELECT \"-- column\";" + EOL + "go" + EOL + "SELECT 2;" }); + + AssertSql( + """ +SELECT "-- column"; +GO + +SELECT 2; +"""); + } + + [ConditionalFact] + public virtual void SqlOperation_handles_brackets_inside_block_comment() + { + Generate( + new SqlOperation { Sql = "/* [bracket identifier */" + EOL + "SELECT 1;" + EOL + "go" + EOL + "SELECT 2;" }); + + AssertSql( + """ +/* [bracket identifier */ +SELECT 1; +GO + +SELECT 2; +"""); + } + + [ConditionalFact] + public virtual void SqlOperation_handles_double_quotes_inside_block_comment() + { + Generate( + new SqlOperation { Sql = "/* \"double quote */" + EOL + "SELECT 1;" + EOL + "go" + EOL + "SELECT 2;" }); + + AssertSql( + """ +/* "double quote */ +SELECT 1; +GO + +SELECT 2; +"""); + } + + [ConditionalFact] + public virtual void SqlOperation_handles_single_quote_inside_block_comment() + { + Generate( + new SqlOperation { Sql = "/* ' quote */" + EOL + "SELECT 1;" + EOL + "go" + EOL + "SELECT 2;" }); + + AssertSql( + """ +/* ' quote */ +SELECT 1; +GO + +SELECT 2; +"""); + } + + [ConditionalFact] + public virtual void SqlOperation_handles_quotes_inside_line_comment() + { + Generate( + new SqlOperation { Sql = "-- 'quote' [bracket] \"double\"" + EOL + "SELECT 1;" + EOL + "go" + EOL + "SELECT 2;" }); + + AssertSql( + """ +-- 'quote' [bracket] "double" +SELECT 1; +GO + +SELECT 2; +"""); + } + + [ConditionalFact] + public virtual void SqlOperation_handles_brackets_inside_line_comment() + { + Generate( + new SqlOperation { Sql = "-- [column" + EOL + "SELECT 1;" + EOL + "go" + EOL + "SELECT 2;" }); + + AssertSql( + """ +-- [column +SELECT 1; +GO + +SELECT 2; +"""); + } + + [ConditionalFact] + public virtual void SqlOperation_handles_double_quotes_inside_line_comment() + { + Generate( + new SqlOperation { Sql = "-- \"column" + EOL + "SELECT 1;" + EOL + "go" + EOL + "SELECT 2;" }); + + AssertSql( + """ +-- "column +SELECT 1; +GO + +SELECT 2; +"""); + } + + [ConditionalFact] + public virtual void SqlOperation_handles_escaped_quotes_in_string() + { + Generate( + new SqlOperation { Sql = "SELECT 'test''s value';" + EOL + "go" + EOL + "SELECT 2;" }); + + AssertSql( + """ +SELECT 'test''s value'; +GO + +SELECT 2; +"""); + } + + [ConditionalFact] + public virtual void SqlOperation_handles_multiple_delimiters_in_string() + { + Generate( + new SqlOperation { Sql = "SELECT '[bracket] and \"quote\"';" + EOL + "go" + EOL + "SELECT 2;" }); + + AssertSql( + """ +SELECT '[bracket] and "quote"'; +GO + +SELECT 2; +"""); + } + public override void InsertDataOperation_all_args_spatial() { base.InsertDataOperation_all_args_spatial();