diff --git a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs index c1d3e2b8a46..c05999a9e70 100644 --- a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs +++ b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs @@ -1388,6 +1388,16 @@ protected override void Generate( builder.EndCommand(); } + private enum ParsingState + { + Normal, + Quoted, + InBlockComment, + MaybeLineComment, + MaybeBlockCommentStart, + MaybeBlockCommentEnd + } + /// /// Builds commands for the given by making calls on the given /// , and then terminates the final command. @@ -1402,12 +1412,20 @@ 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 + // Reset "Maybe" states at line start + if (state == ParsingState.MaybeLineComment + || state == ParsingState.MaybeBlockCommentStart + || state == ParsingState.MaybeBlockCommentEnd) + { + state = state == ParsingState.MaybeBlockCommentEnd ? ParsingState.InBlockComment : ParsingState.Normal; + } + + if (state == ParsingState.Normal && trimmed.StartsWith("GO", StringComparison.OrdinalIgnoreCase) && (trimmed.Length == 2 || char.IsWhiteSpace(trimmed[2]))) @@ -1427,30 +1445,73 @@ protected override void Generate(SqlOperation operation, IModel? model, Migratio } else { - var commentStart = false; foreach (var c in trimmed) { - switch (c) + // Handle MaybeLineComment and MaybeBlockCommentStart first + // When transitioning to Normal, fall through to process the current character + if (state == ParsingState.MaybeLineComment) + { + if (c == '-') + { + goto LineEnd; + } + state = ParsingState.Normal; + } + else if (state == ParsingState.MaybeBlockCommentStart) + { + if (c == '*') + { + state = ParsingState.InBlockComment; + continue; + } + state = ParsingState.Normal; + } + + switch (state) { - case '\'': - quoted = !quoted; - commentStart = false; + case ParsingState.Normal when c == '/': + state = ParsingState.MaybeBlockCommentStart; + break; + + case ParsingState.Normal when c == '\'': + state = ParsingState.Quoted; + break; + + case ParsingState.Normal when c == '-': + state = ParsingState.MaybeLineComment; + break; + + case ParsingState.Normal: break; - case '-': - if (!quoted) - { - if (commentStart) - { - goto LineEnd; - } - commentStart = true; - } + case ParsingState.Quoted when c == '\'': + state = ParsingState.Normal; + break; + + case ParsingState.Quoted: + break; + case ParsingState.InBlockComment when c == '*': + state = ParsingState.MaybeBlockCommentEnd; break; - default: - commentStart = false; + + case ParsingState.InBlockComment: + break; + + case ParsingState.MaybeBlockCommentEnd when c == '/': + state = ParsingState.Normal; + break; + + case ParsingState.MaybeBlockCommentEnd when c == '*': + // Stay in MaybeBlockCommentEnd for consecutive asterisks break; + + case ParsingState.MaybeBlockCommentEnd: + state = ParsingState.InBlockComment; + break; + + default: + throw new UnreachableException(); } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/SqlServerMigrationsSqlGeneratorTest.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/SqlServerMigrationsSqlGeneratorTest.cs index 22ddb64c6bf..a9b10129a55 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/SqlServerMigrationsSqlGeneratorTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/SqlServerMigrationsSqlGeneratorTest.cs @@ -800,6 +800,207 @@ 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; +"""); + } + public override void InsertDataOperation_all_args_spatial() { base.InsertDataOperation_all_args_spatial();