From 98f51739cc6ee6ef1d5317d7bfe418da4833db47 Mon Sep 17 00:00:00 2001 From: Smit Patel Date: Thu, 20 May 2021 11:25:25 -0700 Subject: [PATCH] SqlServer: Add Cast to (n)varchar(max) when injecting Concat for multi-line seed data Resolves #24112 --- .../SqlServerMigrationsSqlGenerator.cs | 117 +++++++++++++++++- .../Internal/SqlServerStringTypeMapping.cs | 22 +++- .../SqlServerMigrationsSqlGeneratorTest.cs | 2 +- .../Storage/SqlServerStringTypeMappingTest.cs | 32 ++--- 4 files changed, 153 insertions(+), 20 deletions(-) diff --git a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs index 0d1299d90a3..db6b8e113d7 100644 --- a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs +++ b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs @@ -1877,6 +1877,7 @@ protected override void ForeignKeyAction(ReferentialAction referentialAction, Mi bool omitVariableDeclarations = false) { var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string)); + var useOldBehavior = AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue24112", out var enabled) && enabled; string schemaLiteral; if (schema == null) @@ -1923,7 +1924,121 @@ protected override void ForeignKeyAction(ReferentialAction referentialAction, Mi builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); string Literal(string s) - => stringTypeMapping.GenerateSqlLiteral(s); + => useOldBehavior + ? stringTypeMapping.GenerateSqlLiteral(s) + : SqlLiteral(s); + + static string SqlLiteral(string value) + { + var builder = new StringBuilder(); + + var start = 0; + int i; + int length; + var openApostrophe = false; + var lastConcatStartPoint = 0; + var concatCount = 1; + var concatStartList = new List(); + for (i = 0; i < value.Length; i++) + { + var lineFeed = value[i] == '\n'; + var carriageReturn = value[i] == '\r'; + var apostrophe = value[i] == '\''; + if (lineFeed || carriageReturn || apostrophe) + { + length = i - start; + if (length != 0) + { + if (!openApostrophe) + { + AddConcatOperatorIfNeeded(); + builder.Append("N\'"); + openApostrophe = true; + } + + builder.Append(value.AsSpan().Slice(start, length)); + } + + if (lineFeed || carriageReturn) + { + if (openApostrophe) + { + builder.Append('\''); + openApostrophe = false; + } + + AddConcatOperatorIfNeeded(); + builder + .Append("NCHAR(") + .Append(lineFeed ? "10" : "13") + .Append(')'); + } + else if (apostrophe) + { + if (!openApostrophe) + { + AddConcatOperatorIfNeeded(); + builder.Append("N'"); + openApostrophe = true; + } + builder.Append("''"); + } + start = i + 1; + } + } + length = i - start; + if (length != 0) + { + if (!openApostrophe) + { + AddConcatOperatorIfNeeded(); + builder.Append("N\'"); + openApostrophe = true; + } + + builder.Append(value.AsSpan().Slice(start, length)); + } + + if (openApostrophe) + { + builder.Append('\''); + } + + for (var j = concatStartList.Count - 1; j >= 0; j--) + { + builder.Insert(concatStartList[j], "CONCAT("); + builder.Append(')'); + } + + if (builder.Length == 0) + { + builder.Append("N''"); + } + + var result = builder.ToString(); + + return result; + + void AddConcatOperatorIfNeeded() + { + if (builder.Length != 0) + { + builder.Append(", "); + concatCount++; + + if (concatCount == 2) + { + concatStartList.Add(lastConcatStartPoint); + } + + if (concatCount == 254) + { + lastConcatStartPoint = builder.Length; + concatCount = 1; + } + } + } + } } /// diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs index 5ef1adf03a6..5e57aca4c24 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs @@ -176,6 +176,8 @@ protected override string GenerateNonNullSqlLiteral(object value) var concatCount = 1; var concatStartList = new List(); var useOldBehavior = AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue23518", out var enabled) && enabled; + var useOldBehavior2 = AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue24112", out var enabled2) && enabled2; + var castApplied = false; for (i = 0; i < stringValue.Length; i++) { var lineFeed = stringValue[i] == '\n'; @@ -277,8 +279,12 @@ protected override string GenerateNonNullSqlLiteral(object value) { for (var j = concatStartList.Count - 1; j >= 0; j--) { - builder.Insert(concatStartList[j], "CONCAT(") - .Append(')'); + if (castApplied && j == 0) + { + builder.Insert(concatStartList[j], "CAST("); + } + builder.Insert(concatStartList[j], "CONCAT("); + builder.Append(')'); } } @@ -298,6 +304,18 @@ void AddConcatOperatorIfNeeded() { if (builder.Length != 0) { + if (!useOldBehavior2 + && !castApplied) + { + builder.Append(" AS "); + if (IsUnicode) + { + builder.Append("N"); + } + builder.Append("VARCHAR(MAX))"); + castApplied = true; + } + builder.Append(", "); if (useOldBehavior) { diff --git a/test/EFCore.SqlServer.Tests/Migrations/SqlServerMigrationsSqlGeneratorTest.cs b/test/EFCore.SqlServer.Tests/Migrations/SqlServerMigrationsSqlGeneratorTest.cs index ec74184cbef..2d704ac4e2b 100644 --- a/test/EFCore.SqlServer.Tests/Migrations/SqlServerMigrationsSqlGeneratorTest.cs +++ b/test/EFCore.SqlServer.Tests/Migrations/SqlServerMigrationsSqlGeneratorTest.cs @@ -963,7 +963,7 @@ public override void DefaultValue_with_line_breaks(bool isUnicode) var storeType = isUnicode ? "nvarchar(max)" : "varchar(max)"; var unicodePrefix = isUnicode ? "N" : string.Empty; var expectedSql = @$"CREATE TABLE [dbo].[TestLineBreaks] ( - [TestDefaultValue] {storeType} NOT NULL DEFAULT CONCAT({unicodePrefix}CHAR(13), {unicodePrefix}CHAR(10), {unicodePrefix}'Various Line', {unicodePrefix}CHAR(13), {unicodePrefix}'Breaks', {unicodePrefix}CHAR(10)) + [TestDefaultValue] {storeType} NOT NULL DEFAULT CONCAT(CAST({unicodePrefix}CHAR(13) AS {unicodePrefix}VARCHAR(MAX)), {unicodePrefix}CHAR(10), {unicodePrefix}'Various Line', {unicodePrefix}CHAR(13), {unicodePrefix}'Breaks', {unicodePrefix}CHAR(10)) ); "; AssertSql(expectedSql); diff --git a/test/EFCore.SqlServer.Tests/Storage/SqlServerStringTypeMappingTest.cs b/test/EFCore.SqlServer.Tests/Storage/SqlServerStringTypeMappingTest.cs index 3847365ecba..d66b207754f 100644 --- a/test/EFCore.SqlServer.Tests/Storage/SqlServerStringTypeMappingTest.cs +++ b/test/EFCore.SqlServer.Tests/Storage/SqlServerStringTypeMappingTest.cs @@ -19,14 +19,14 @@ public class SqlServerStringTypeMappingTest [InlineData("I'm lovin' it", "'I''m lovin'' it'")] [InlineData("\r", "CHAR(13)")] [InlineData("\n", "CHAR(10)")] - [InlineData("\r\n", "CONCAT(CHAR(13), CHAR(10))")] - [InlineData("\n'sup", "CONCAT(CHAR(10), '''sup')")] - [InlineData("I'm\n", "CONCAT('I''m', CHAR(10))")] - [InlineData("lovin'\n", "CONCAT('lovin''', CHAR(10))")] - [InlineData("it\n", "CONCAT('it', CHAR(10))")] - [InlineData("\nit", "CONCAT(CHAR(10), 'it')")] - [InlineData("\nit\n", "CONCAT(CHAR(10), 'it', CHAR(10))")] - [InlineData("'\n", "CONCAT('''', CHAR(10))")] + [InlineData("\r\n", "CONCAT(CAST(CHAR(13) AS VARCHAR(MAX)), CHAR(10))")] + [InlineData("\n'sup", "CONCAT(CAST(CHAR(10) AS VARCHAR(MAX)), '''sup')")] + [InlineData("I'm\n", "CONCAT(CAST('I''m' AS VARCHAR(MAX)), CHAR(10))")] + [InlineData("lovin'\n", "CONCAT(CAST('lovin''' AS VARCHAR(MAX)), CHAR(10))")] + [InlineData("it\n", "CONCAT(CAST('it' AS VARCHAR(MAX)), CHAR(10))")] + [InlineData("\nit", "CONCAT(CAST(CHAR(10) AS VARCHAR(MAX)), 'it')")] + [InlineData("\nit\n", "CONCAT(CAST(CHAR(10) AS VARCHAR(MAX)), 'it', CHAR(10))")] + [InlineData("'\n", "CONCAT(CAST('''' AS VARCHAR(MAX)), CHAR(10))")] public void GenerateProviderValueSqlLiteral_works(string value, string expected) { var mapping = new SqlServerStringTypeMapping("varchar(max)"); @@ -45,14 +45,14 @@ public void GenerateProviderValueSqlLiteral_works(string value, string expected) [InlineData("I'm lovin' it", "N'I''m lovin'' it'")] [InlineData("\r", "NCHAR(13)")] [InlineData("\n", "NCHAR(10)")] - [InlineData("\r\n", "CONCAT(NCHAR(13), NCHAR(10))")] - [InlineData("\n'sup", "CONCAT(NCHAR(10), N'''sup')")] - [InlineData("I'm\n", "CONCAT(N'I''m', NCHAR(10))")] - [InlineData("lovin'\n", "CONCAT(N'lovin''', NCHAR(10))")] - [InlineData("it\n", "CONCAT(N'it', NCHAR(10))")] - [InlineData("\nit", "CONCAT(NCHAR(10), N'it')")] - [InlineData("\nit\n", "CONCAT(NCHAR(10), N'it', NCHAR(10))")] - [InlineData("'\n", "CONCAT(N'''', NCHAR(10))")] + [InlineData("\r\n", "CONCAT(CAST(NCHAR(13) AS NVARCHAR(MAX)), NCHAR(10))")] + [InlineData("\n'sup", "CONCAT(CAST(NCHAR(10) AS NVARCHAR(MAX)), '''sup')")] + [InlineData("I'm\n", "CONCAT(CAST('I''m' AS NVARCHAR(MAX)), NCHAR(10))")] + [InlineData("lovin'\n", "CONCAT(CAST('lovin''' AS NVARCHAR(MAX)), NCHAR(10))")] + [InlineData("it\n", "CONCAT(CAST('it' AS NVARCHAR(MAX)), NCHAR(10))")] + [InlineData("\nit", "CONCAT(CAST(NCHAR(10) AS NVARCHAR(MAX)), 'it')")] + [InlineData("\nit\n", "CONCAT(CAST(NCHAR(10) AS NVARCHAR(MAX)), 'it', NCHAR(10))")] + [InlineData("'\n", "CONCAT(CAST('''' AS NVARCHAR(MAX)), NCHAR(10))")] public void GenerateProviderValueSqlLiteral_works_unicode(string value, string expected) { var mapping = new SqlServerStringTypeMapping("nvarchar(max)", unicode: true);