From 480995271826a8fc5d78e2320c8db462b59a3bb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jiri=20Cincura=20=E2=86=B9?= Date: Wed, 21 Aug 2024 22:53:05 +0200 Subject: [PATCH 1/2] Fix ESCAPE clause for Azure Synapse. (#34463) --- ...qlServerSqlTranslatingExpressionVisitor.cs | 148 +++-- .../Query/FunkyDataQueryAzureSynapseTest.cs | 599 ++++++++++++++++++ .../Query/FunkyDataQuerySqlServerBaseTest.cs | 27 + .../Query/FunkyDataQuerySqlServerTest.cs | 16 +- .../TestUtilities/AzureSynapseTestStore.cs | 50 ++ .../AzureSynapseTestStoreFactory.cs | 23 + ...ServerDbContextOptionsBuilderExtensions.cs | 34 + .../TestUtilities/SqlServerTestStore.cs | 2 +- 8 files changed, 821 insertions(+), 78 deletions(-) create mode 100644 test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQueryAzureSynapseTest.cs create mode 100644 test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQuerySqlServerBaseTest.cs create mode 100644 test/EFCore.SqlServer.FunctionalTests/TestUtilities/AzureSynapseTestStore.cs create mode 100644 test/EFCore.SqlServer.FunctionalTests/TestUtilities/AzureSynapseTestStoreFactory.cs diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs index b17c396c578..039d146feb2 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs @@ -297,20 +297,8 @@ bool TryTranslateStartsEndsWithContains( // (but SqlNullabilityProcess will convert this to a true constant if the instance is non-nullable) "" => _sqlExpressionFactory.Like(translatedInstance, _sqlExpressionFactory.Constant("%")), - string s => s.Any(IsLikeWildChar) - ? _sqlExpressionFactory.Like( - translatedInstance, - _sqlExpressionFactory.Constant( - methodType switch - { - StartsEndsWithContains.StartsWith => EscapeLikePattern(s) + '%', - StartsEndsWithContains.EndsWith => '%' + EscapeLikePattern(s), - StartsEndsWithContains.Contains => $"%{EscapeLikePattern(s)}%", - - _ => throw new ArgumentOutOfRangeException(nameof(methodType), methodType, null) - }), - _sqlExpressionFactory.Constant(LikeEscapeString)) - : _sqlExpressionFactory.Like( + string s when !s.Any(IsLikeWildChar) + => _sqlExpressionFactory.Like( translatedInstance, _sqlExpressionFactory.Constant( methodType switch @@ -322,6 +310,24 @@ bool TryTranslateStartsEndsWithContains( _ => throw new ArgumentOutOfRangeException(nameof(methodType), methodType, null) })), + // Azure Synapse does not support ESCAPE clause in LIKE + // fallback to translation like with column/expression + string s when _sqlServerSingletonOptions.EngineType == SqlServerEngineType.AzureSynapse + => TranslateWithoutLike(patternIsNonEmptyConstantString: true), + + string s => _sqlExpressionFactory.Like( + translatedInstance, + _sqlExpressionFactory.Constant( + methodType switch + { + StartsEndsWithContains.StartsWith => EscapeLikePattern(s) + '%', + StartsEndsWithContains.EndsWith => '%' + EscapeLikePattern(s), + StartsEndsWithContains.Contains => $"%{EscapeLikePattern(s)}%", + + _ => throw new ArgumentOutOfRangeException(nameof(methodType), methodType, null) + }), + _sqlExpressionFactory.Constant(LikeEscapeString)), + _ => throw new UnreachableException() }; @@ -329,7 +335,10 @@ bool TryTranslateStartsEndsWithContains( } case SqlParameterExpression patternParameter - when patternParameter.Name.StartsWith(QueryCompilationContext.QueryParameterPrefix, StringComparison.Ordinal): + when patternParameter.Name.StartsWith(QueryCompilationContext.QueryParameterPrefix, StringComparison.Ordinal) + // Azure Synapse does not support ESCAPE clause in LIKE + // fall through to translation like with column/expression + && _sqlServerSingletonOptions.EngineType != SqlServerEngineType.AzureSynapse: { // The pattern is a parameter, register a runtime parameter that will contain the rewritten LIKE pattern, where // all special characters have been escaped. @@ -356,23 +365,29 @@ when patternParameter.Name.StartsWith(QueryCompilationContext.QueryParameterPref default: // The pattern is a column or a complex expression; the possible special characters in the pattern cannot be escaped, // preventing us from translating to LIKE. - translation = methodType switch - { - // For StartsWith/EndsWith, use LEFT or RIGHT instead to extract substring and compare: - // WHERE instance IS NOT NULL AND pattern IS NOT NULL AND LEFT(instance, LEN(pattern)) = pattern - // This is less efficient than LIKE (i.e. StartsWith does an index scan instead of seek), but we have no choice. - // Note that we compensate for the case where both the instance and the pattern are null (null.StartsWith(null)); a - // simple equality would yield true in that case, but we want false. We technically - StartsEndsWithContains.StartsWith or StartsEndsWithContains.EndsWith - => _sqlExpressionFactory.AndAlso( - _sqlExpressionFactory.IsNotNull(translatedInstance), - _sqlExpressionFactory.AndAlso( - _sqlExpressionFactory.IsNotNull(translatedPattern), - _sqlExpressionFactory.Equal( - _sqlExpressionFactory.Function( - methodType is StartsEndsWithContains.StartsWith ? "LEFT" : "RIGHT", - new[] - { + translation = TranslateWithoutLike(); + return true; + } + + SqlExpression TranslateWithoutLike(bool patternIsNonEmptyConstantString = false) + { + return methodType switch + { + // For StartsWith/EndsWith, use LEFT or RIGHT instead to extract substring and compare: + // WHERE instance IS NOT NULL AND pattern IS NOT NULL AND LEFT(instance, LEN(pattern)) = pattern + // This is less efficient than LIKE (i.e. StartsWith does an index scan instead of seek), but we have no choice. + // Note that we compensate for the case where both the instance and the pattern are null (null.StartsWith(null)); a + // simple equality would yield true in that case, but we want false. We technically + StartsEndsWithContains.StartsWith or StartsEndsWithContains.EndsWith + => _sqlExpressionFactory.AndAlso( + _sqlExpressionFactory.IsNotNull(translatedInstance), + _sqlExpressionFactory.AndAlso( + _sqlExpressionFactory.IsNotNull(translatedPattern), + _sqlExpressionFactory.Equal( + _sqlExpressionFactory.Function( + methodType is StartsEndsWithContains.StartsWith ? "LEFT" : "RIGHT", + new[] + { translatedInstance, _sqlExpressionFactory.Function( "LEN", @@ -380,37 +395,44 @@ StartsEndsWithContains.StartsWith or StartsEndsWithContains.EndsWith nullable: true, argumentsPropagateNullability: new[] { true }, typeof(int)) - }, - nullable: true, - argumentsPropagateNullability: new[] { true, true }, - typeof(string), - stringTypeMapping), - translatedPattern))), - - // For Contains, just use CHARINDEX and check if the result is greater than 0. - // Add a check to return null when the pattern is an empty string (and the string isn't null) - StartsEndsWithContains.Contains - => _sqlExpressionFactory.AndAlso( - _sqlExpressionFactory.IsNotNull(translatedInstance), - _sqlExpressionFactory.AndAlso( - _sqlExpressionFactory.IsNotNull(translatedPattern), - _sqlExpressionFactory.OrElse( - _sqlExpressionFactory.GreaterThan( - _sqlExpressionFactory.Function( - "CHARINDEX", - new[] { translatedPattern, translatedInstance }, - nullable: true, - argumentsPropagateNullability: new[] { true, true }, - typeof(int)), - _sqlExpressionFactory.Constant(0)), - _sqlExpressionFactory.Like( - translatedPattern, - _sqlExpressionFactory.Constant(string.Empty, stringTypeMapping))))), - - _ => throw new UnreachableException() - }; - - return true; + }, + nullable: true, + argumentsPropagateNullability: new[] { true, true }, + typeof(string), + stringTypeMapping), + translatedPattern))), + + // For Contains, just use CHARINDEX and check if the result is greater than 0. + StartsEndsWithContains.Contains when patternIsNonEmptyConstantString + => _sqlExpressionFactory.AndAlso( + _sqlExpressionFactory.IsNotNull(translatedInstance), + CharIndexGreaterThanZero()), + + // For Contains, just use CHARINDEX and check if the result is greater than 0. + // Add a check to return null when the pattern is an empty string (and the string isn't null) + StartsEndsWithContains.Contains + => _sqlExpressionFactory.AndAlso( + _sqlExpressionFactory.IsNotNull(translatedInstance), + _sqlExpressionFactory.AndAlso( + _sqlExpressionFactory.IsNotNull(translatedPattern), + _sqlExpressionFactory.OrElse( + CharIndexGreaterThanZero(), + _sqlExpressionFactory.Like( + translatedPattern, + _sqlExpressionFactory.Constant(string.Empty, stringTypeMapping))))), + + _ => throw new UnreachableException() + }; + + SqlExpression CharIndexGreaterThanZero() + => _sqlExpressionFactory.GreaterThan( + _sqlExpressionFactory.Function( + "CHARINDEX", + new[] { translatedPattern, translatedInstance }, + nullable: true, + argumentsPropagateNullability: new[] { true, true }, + typeof(int)), + _sqlExpressionFactory.Constant(0)); } } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQueryAzureSynapseTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQueryAzureSynapseTest.cs new file mode 100644 index 00000000000..1101ad3c9d1 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQueryAzureSynapseTest.cs @@ -0,0 +1,599 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query; + +#nullable disable + +public class FunkyDataQueryAzureSynapseTest : FunkyDataQuerySqlServerBaseTest +{ + public FunkyDataQueryAzureSynapseTest(FunkyDataQueryAzureSynapseFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture, testOutputHelper) + { + } + + public override async Task String_contains_on_argument_with_wildcard_constant(bool async) + { + await base.String_contains_on_argument_with_wildcard_constant(async); + + AssertSql( + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND CHARINDEX(N'%B', [f].[FirstName]) > 0 +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND CHARINDEX(N'a_', [f].[FirstName]) > 0 +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE 0 = 1 +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND CHARINDEX(N'_Ba_', [f].[FirstName]) > 0 +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NULL OR CHARINDEX(N'%B%a%r', [f].[FirstName]) <= 0 +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NULL +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +"""); + } + + public override async Task String_contains_on_argument_with_wildcard_parameter(bool async) + { + await base.String_contains_on_argument_with_wildcard_parameter(async); + + AssertSql( + """ +@__prm1_0='%B' (Size = 4000) + +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND (CHARINDEX(@__prm1_0, [f].[FirstName]) > 0 OR @__prm1_0 LIKE N'') +""", + // + """ +@__prm2_0='a_' (Size = 4000) + +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND (CHARINDEX(@__prm2_0, [f].[FirstName]) > 0 OR @__prm2_0 LIKE N'') +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE 0 = 1 +""", + // + """ +@__prm4_0='' (Size = 4000) + +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND (CHARINDEX(@__prm4_0, [f].[FirstName]) > 0 OR @__prm4_0 LIKE N'') +""", + // + """ +@__prm5_0='_Ba_' (Size = 4000) + +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND (CHARINDEX(@__prm5_0, [f].[FirstName]) > 0 OR @__prm5_0 LIKE N'') +""", + // + """ +@__prm6_0='%B%a%r' (Size = 4000) + +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NULL OR (CHARINDEX(@__prm6_0, [f].[FirstName]) <= 0 AND @__prm6_0 NOT LIKE N'') +""", + // + """ +@__prm7_0='' (Size = 4000) + +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NULL OR (CHARINDEX(@__prm7_0, [f].[FirstName]) <= 0 AND @__prm7_0 NOT LIKE N'') +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +"""); + } + + public override async Task String_contains_on_argument_with_wildcard_column(bool async) + { + await base.String_contains_on_argument_with_wildcard_column(async); + + AssertSql( + """ +SELECT [f].[FirstName] AS [fn], [f0].[LastName] AS [ln] +FROM [FunkyCustomers] AS [f] +CROSS JOIN [FunkyCustomers] AS [f0] +WHERE [f].[FirstName] IS NOT NULL AND [f0].[LastName] IS NOT NULL AND (CHARINDEX([f0].[LastName], [f].[FirstName]) > 0 OR [f0].[LastName] LIKE N'') +"""); + } + + public override async Task String_contains_on_argument_with_wildcard_column_negated(bool async) + { + await base.String_contains_on_argument_with_wildcard_column_negated(async); + + AssertSql( + """ +SELECT [f].[FirstName] AS [fn], [f0].[LastName] AS [ln] +FROM [FunkyCustomers] AS [f] +CROSS JOIN [FunkyCustomers] AS [f0] +WHERE [f].[FirstName] IS NULL OR [f0].[LastName] IS NULL OR (CHARINDEX([f0].[LastName], [f].[FirstName]) <= 0 AND [f0].[LastName] NOT LIKE N'') +"""); + } + + public override async Task String_starts_with_on_argument_with_wildcard_constant(bool async) + { + await base.String_starts_with_on_argument_with_wildcard_constant(async); + + AssertSql( + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND LEFT([f].[FirstName], LEN(N'%B')) = N'%B' +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND LEFT([f].[FirstName], LEN(N'_B')) = N'_B' +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE 0 = 1 +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND LEFT([f].[FirstName], LEN(N'_Ba_')) = N'_Ba_' +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NULL OR LEFT([f].[FirstName], LEN(N'%B%a%r')) <> N'%B%a%r' +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NULL +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +"""); + } + + public override async Task String_starts_with_on_argument_with_wildcard_parameter(bool async) + { + await base.String_starts_with_on_argument_with_wildcard_parameter(async); + + AssertSql( + """ +@__prm1_0='%B' (Size = 4000) + +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND LEFT([f].[FirstName], LEN(@__prm1_0)) = @__prm1_0 +""", + // + """ +@__prm2_0='_B' (Size = 4000) + +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND LEFT([f].[FirstName], LEN(@__prm2_0)) = @__prm2_0 +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE 0 = 1 +""", + // + """ +@__prm4_0='' (Size = 4000) + +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND LEFT([f].[FirstName], LEN(@__prm4_0)) = @__prm4_0 +""", + // + """ +@__prm5_0='_Ba_' (Size = 4000) + +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND LEFT([f].[FirstName], LEN(@__prm5_0)) = @__prm5_0 +""", + // + """ +@__prm6_0='%B%a%r' (Size = 4000) + +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NULL OR LEFT([f].[FirstName], LEN(@__prm6_0)) <> @__prm6_0 +""", + // + """ +@__prm7_0='' (Size = 4000) + +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NULL OR LEFT([f].[FirstName], LEN(@__prm7_0)) <> @__prm7_0 +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +"""); + } + + public override async Task String_starts_with_on_argument_with_bracket(bool async) + { + await base.String_starts_with_on_argument_with_bracket(async); + + AssertSql( + """ +SELECT [f].[Id], [f].[FirstName], [f].[LastName], [f].[NullableBool] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND LEFT([f].[FirstName], LEN(N'[')) = N'[' +""", + // + """ +SELECT [f].[Id], [f].[FirstName], [f].[LastName], [f].[NullableBool] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND LEFT([f].[FirstName], LEN(N'B[')) = N'B[' +""", + // + """ +SELECT [f].[Id], [f].[FirstName], [f].[LastName], [f].[NullableBool] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND LEFT([f].[FirstName], LEN(N'B[[a^')) = N'B[[a^' +""", + // + """ +@__prm1_0='[' (Size = 4000) + +SELECT [f].[Id], [f].[FirstName], [f].[LastName], [f].[NullableBool] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND LEFT([f].[FirstName], LEN(@__prm1_0)) = @__prm1_0 +""", + // + """ +@__prm2_0='B[' (Size = 4000) + +SELECT [f].[Id], [f].[FirstName], [f].[LastName], [f].[NullableBool] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND LEFT([f].[FirstName], LEN(@__prm2_0)) = @__prm2_0 +""", + // + """ +@__prm3_0='B[[a^' (Size = 4000) + +SELECT [f].[Id], [f].[FirstName], [f].[LastName], [f].[NullableBool] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND LEFT([f].[FirstName], LEN(@__prm3_0)) = @__prm3_0 +""", + // + """ +SELECT [f].[Id], [f].[FirstName], [f].[LastName], [f].[NullableBool] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND [f].[LastName] IS NOT NULL AND LEFT([f].[FirstName], LEN([f].[LastName])) = [f].[LastName] +"""); + } + + public override async Task String_starts_with_on_argument_with_wildcard_column(bool async) + { + await base.String_starts_with_on_argument_with_wildcard_column(async); + + AssertSql( + """ +SELECT [f].[FirstName] AS [fn], [f0].[LastName] AS [ln] +FROM [FunkyCustomers] AS [f] +CROSS JOIN [FunkyCustomers] AS [f0] +WHERE [f].[FirstName] IS NOT NULL AND [f0].[LastName] IS NOT NULL AND LEFT([f].[FirstName], LEN([f0].[LastName])) = [f0].[LastName] +"""); + } + + public override async Task String_starts_with_on_argument_with_wildcard_column_negated(bool async) + { + await base.String_starts_with_on_argument_with_wildcard_column_negated(async); + + AssertSql( + """ +SELECT [f].[FirstName] AS [fn], [f0].[LastName] AS [ln] +FROM [FunkyCustomers] AS [f] +CROSS JOIN [FunkyCustomers] AS [f0] +WHERE [f].[FirstName] IS NULL OR [f0].[LastName] IS NULL OR LEFT([f].[FirstName], LEN([f0].[LastName])) <> [f0].[LastName] +"""); + } + + public override async Task String_ends_with_on_argument_with_wildcard_constant(bool async) + { + await base.String_ends_with_on_argument_with_wildcard_constant(async); + + AssertSql( + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND RIGHT([f].[FirstName], LEN(N'%r')) = N'%r' +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND RIGHT([f].[FirstName], LEN(N'r_')) = N'r_' +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE 0 = 1 +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND RIGHT([f].[FirstName], LEN(N'_r_')) = N'_r_' +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NULL OR RIGHT([f].[FirstName], LEN(N'a%r%')) <> N'a%r%' +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NULL +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +"""); + } + + public override async Task String_ends_with_on_argument_with_wildcard_parameter(bool async) + { + await base.String_ends_with_on_argument_with_wildcard_parameter(async); + + AssertSql( + """ +@__prm1_0='%r' (Size = 4000) + +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND RIGHT([f].[FirstName], LEN(@__prm1_0)) = @__prm1_0 +""", + // + """ +@__prm2_0='r_' (Size = 4000) + +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND RIGHT([f].[FirstName], LEN(@__prm2_0)) = @__prm2_0 +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE 0 = 1 +""", + // + """ +@__prm4_0='' (Size = 4000) + +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND RIGHT([f].[FirstName], LEN(@__prm4_0)) = @__prm4_0 +""", + // + """ +@__prm5_0='_r_' (Size = 4000) + +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND RIGHT([f].[FirstName], LEN(@__prm5_0)) = @__prm5_0 +""", + // + """ +@__prm6_0='a%r%' (Size = 4000) + +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NULL OR RIGHT([f].[FirstName], LEN(@__prm6_0)) <> @__prm6_0 +""", + // + """ +@__prm7_0='' (Size = 4000) + +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NULL OR RIGHT([f].[FirstName], LEN(@__prm7_0)) <> @__prm7_0 +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +"""); + } + + public override async Task String_ends_with_on_argument_with_wildcard_column(bool async) + { + await base.String_ends_with_on_argument_with_wildcard_column(async); + + AssertSql( + """ +SELECT [f].[FirstName] AS [fn], [f0].[LastName] AS [ln] +FROM [FunkyCustomers] AS [f] +CROSS JOIN [FunkyCustomers] AS [f0] +WHERE [f].[FirstName] IS NOT NULL AND [f0].[LastName] IS NOT NULL AND RIGHT([f].[FirstName], LEN([f0].[LastName])) = [f0].[LastName] +"""); + } + + public override async Task String_ends_with_on_argument_with_wildcard_column_negated(bool async) + { + await base.String_ends_with_on_argument_with_wildcard_column_negated(async); + + AssertSql( + """ +SELECT [f].[FirstName] AS [fn], [f0].[LastName] AS [ln] +FROM [FunkyCustomers] AS [f] +CROSS JOIN [FunkyCustomers] AS [f0] +WHERE [f].[FirstName] IS NULL OR [f0].[LastName] IS NULL OR RIGHT([f].[FirstName], LEN([f0].[LastName])) <> [f0].[LastName] +"""); + } + + public override async Task String_ends_with_inside_conditional(bool async) + { + await base.String_ends_with_inside_conditional(async); + + AssertSql( + """ +SELECT [f].[FirstName] AS [fn], [f0].[LastName] AS [ln] +FROM [FunkyCustomers] AS [f] +CROSS JOIN [FunkyCustomers] AS [f0] +WHERE CASE + WHEN [f].[FirstName] IS NOT NULL AND [f0].[LastName] IS NOT NULL AND RIGHT([f].[FirstName], LEN([f0].[LastName])) = [f0].[LastName] THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END = CAST(1 AS bit) +"""); + } + + public override async Task String_ends_with_inside_conditional_negated(bool async) + { + await base.String_ends_with_inside_conditional_negated(async); + + AssertSql( + """ +SELECT [f].[FirstName] AS [fn], [f0].[LastName] AS [ln] +FROM [FunkyCustomers] AS [f] +CROSS JOIN [FunkyCustomers] AS [f0] +WHERE CASE + WHEN [f].[FirstName] IS NULL OR [f0].[LastName] IS NULL OR RIGHT([f].[FirstName], LEN([f0].[LastName])) <> [f0].[LastName] THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END = CAST(1 AS bit) +"""); + } + + public override async Task String_ends_with_equals_nullable_column(bool async) + { + await base.String_ends_with_equals_nullable_column(async); + + AssertSql( + """ +SELECT [f].[Id], [f].[FirstName], [f].[LastName], [f].[NullableBool], [f0].[Id], [f0].[FirstName], [f0].[LastName], [f0].[NullableBool] +FROM [FunkyCustomers] AS [f] +CROSS JOIN [FunkyCustomers] AS [f0] +WHERE CASE + WHEN [f].[FirstName] IS NOT NULL AND [f0].[LastName] IS NOT NULL AND RIGHT([f].[FirstName], LEN([f0].[LastName])) = [f0].[LastName] THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END = [f].[NullableBool] +"""); + } + + public override async Task String_ends_with_not_equals_nullable_column(bool async) + { + await base.String_ends_with_not_equals_nullable_column(async); + + AssertSql( + """ +SELECT [f].[Id], [f].[FirstName], [f].[LastName], [f].[NullableBool], [f0].[Id], [f0].[FirstName], [f0].[LastName], [f0].[NullableBool] +FROM [FunkyCustomers] AS [f] +CROSS JOIN [FunkyCustomers] AS [f0] +WHERE CASE + WHEN [f].[FirstName] IS NOT NULL AND [f0].[LastName] IS NOT NULL AND RIGHT([f].[FirstName], LEN([f0].[LastName])) = [f0].[LastName] THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END <> [f].[NullableBool] OR [f].[NullableBool] IS NULL +"""); + } + + public override async Task String_FirstOrDefault_and_LastOrDefault(bool async) + { + await base.String_FirstOrDefault_and_LastOrDefault(async); + + AssertSql( + """ +SELECT SUBSTRING([f].[FirstName], 1, 1) AS [first], SUBSTRING([f].[FirstName], LEN([f].[FirstName]), 1) AS [last] +FROM [FunkyCustomers] AS [f] +ORDER BY [f].[Id] +"""); + } + + public override async Task String_Contains_and_StartsWith_with_same_parameter(bool async) + { + await base.String_Contains_and_StartsWith_with_same_parameter(async); + + AssertSql( + """ +@__s_0='B' (Size = 4000) + +SELECT [f].[Id], [f].[FirstName], [f].[LastName], [f].[NullableBool] +FROM [FunkyCustomers] AS [f] +WHERE ([f].[FirstName] IS NOT NULL AND (CHARINDEX(@__s_0, [f].[FirstName]) > 0 OR @__s_0 LIKE N'')) OR ([f].[LastName] IS NOT NULL AND LEFT([f].[LastName], LEN(@__s_0)) = @__s_0) +"""); + } + + public class FunkyDataQueryAzureSynapseFixture : FunkyDataQueryFixtureBase, ITestSqlLoggerFactory + { + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected override ITestStoreFactory TestStoreFactory + => AzureSynapseTestStoreFactory.Instance; + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQuerySqlServerBaseTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQuerySqlServerBaseTest.cs new file mode 100644 index 00000000000..9e1793b3ac6 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQuerySqlServerBaseTest.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query; + +#nullable disable + +public abstract class FunkyDataQuerySqlServerBaseTest : FunkyDataQueryTestBase + where TFixture : FunkyDataQueryTestBase.FunkyDataQueryFixtureBase, ITestSqlLoggerFactory, new() +{ + public FunkyDataQuerySqlServerBaseTest(TFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + protected override QueryAsserter CreateQueryAsserter(TFixture fixture) + => new RelationalQueryAsserter( + fixture, RewriteExpectedQueryExpression, RewriteServerQueryExpression); + + protected override void ClearLog() + => Fixture.TestSqlLoggerFactory.Clear(); + + protected void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQuerySqlServerTest.cs index b7e8da920f1..1b1a68b03c2 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQuerySqlServerTest.cs @@ -5,19 +5,13 @@ namespace Microsoft.EntityFrameworkCore.Query; #nullable disable -public class FunkyDataQuerySqlServerTest : FunkyDataQueryTestBase +public class FunkyDataQuerySqlServerTest : FunkyDataQuerySqlServerBaseTest { public FunkyDataQuerySqlServerTest(FunkyDataQuerySqlServerFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) + : base(fixture, testOutputHelper) { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } - protected override QueryAsserter CreateQueryAsserter(FunkyDataQuerySqlServerFixture fixture) - => new RelationalQueryAsserter( - fixture, RewriteExpectedQueryExpression, RewriteServerQueryExpression); - public override async Task String_contains_on_argument_with_wildcard_constant(bool async) { await base.String_contains_on_argument_with_wildcard_constant(async); @@ -595,12 +589,6 @@ WHERE [f].[FirstName] LIKE @__s_0_contains ESCAPE N'\' OR [f].[LastName] LIKE @_ """); } - protected override void ClearLog() - => Fixture.TestSqlLoggerFactory.Clear(); - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - public class FunkyDataQuerySqlServerFixture : FunkyDataQueryFixtureBase, ITestSqlLoggerFactory { public TestSqlLoggerFactory TestSqlLoggerFactory diff --git a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/AzureSynapseTestStore.cs b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/AzureSynapseTestStore.cs new file mode 100644 index 00000000000..3b7a628a676 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/AzureSynapseTestStore.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable IDE0022 // Use block body for methods +// ReSharper disable SuggestBaseTypeForParameter +namespace Microsoft.EntityFrameworkCore.TestUtilities; + +public class AzureSynapseTestStore : SqlServerTestStore +{ + public new static async Task GetNorthwindStoreAsync() + => (AzureSynapseTestStore)await SqlServerNorthwindTestStoreFactory.Instance + .GetOrCreate(SqlServerNorthwindTestStoreFactory.Name).InitializeAsync(null, (Func?)null); + + public new static AzureSynapseTestStore GetOrCreate(string name) + => new(name); + + public new static async Task GetOrCreateInitializedAsync(string name) + => (AzureSynapseTestStore)await new AzureSynapseTestStore(name).InitializeSqlServerAsync(null, (Func?)null, null); + + public new static AzureSynapseTestStore GetOrCreateWithInitScript(string name, string initScript) + => new(name, initScript: initScript); + + public new static AzureSynapseTestStore GetOrCreateWithScriptPath( + string name, + string scriptPath, + bool? multipleActiveResultSets = null, + bool shared = true) + => new(name, scriptPath: scriptPath, multipleActiveResultSets: multipleActiveResultSets, shared: shared); + + public new static AzureSynapseTestStore Create(string name, bool useFileName = false) + => new(name, useFileName, shared: false); + + public new static async Task CreateInitializedAsync( + string name, + bool useFileName = false, + bool? multipleActiveResultSets = null) + => (AzureSynapseTestStore)await new AzureSynapseTestStore(name, useFileName, shared: false, multipleActiveResultSets: multipleActiveResultSets) + .InitializeSqlServerAsync(null, (Func?)null, null); + + protected AzureSynapseTestStore(string name, bool useFileName = false, bool? multipleActiveResultSets = null, string? initScript = null, string? scriptPath = null, bool shared = true) + : base(name, useFileName, multipleActiveResultSets, initScript, scriptPath, shared) + { + } + + public override DbContextOptionsBuilder AddProviderOptions(DbContextOptionsBuilder builder) + => (UseConnectionString + ? builder.UseAzureSynapse(ConnectionString, b => b.ApplyConfiguration()) + : builder.UseAzureSynapse(Connection, b => b.ApplyConfiguration())) + .ConfigureWarnings(b => b.Ignore(SqlServerEventId.SavepointsDisabledBecauseOfMARS)); +} diff --git a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/AzureSynapseTestStoreFactory.cs b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/AzureSynapseTestStoreFactory.cs new file mode 100644 index 00000000000..d7e50e56ee4 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/AzureSynapseTestStoreFactory.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.TestUtilities; + +public class AzureSynapseTestStoreFactory : RelationalTestStoreFactory +{ + public static AzureSynapseTestStoreFactory Instance { get; } = new(); + + protected AzureSynapseTestStoreFactory() + { + } + + public override TestStore Create(string storeName) + => AzureSynapseTestStore.Create(storeName); + + public override TestStore GetOrCreate(string storeName) + => AzureSynapseTestStore.GetOrCreate(storeName); + + public override IServiceCollection AddProviderServices(IServiceCollection serviceCollection) + => serviceCollection + .AddEntityFrameworkAzureSynapse(); +} diff --git a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerDbContextOptionsBuilderExtensions.cs b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerDbContextOptionsBuilderExtensions.cs index 0751a0eb769..01998ce74f7 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerDbContextOptionsBuilderExtensions.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerDbContextOptionsBuilderExtensions.cs @@ -21,4 +21,38 @@ public static SqlServerDbContextOptionsBuilder ApplyConfiguration(this SqlServer return optionsBuilder; } + + public static AzureSqlDbContextOptionsBuilder ApplyConfiguration(this AzureSqlDbContextOptionsBuilder optionsBuilder) + { + var maxBatch = TestEnvironment.GetInt(nameof(SqlServerDbContextOptionsBuilder.MaxBatchSize)); + if (maxBatch.HasValue) + { + optionsBuilder.MaxBatchSize(maxBatch.Value); + } + + optionsBuilder.UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery); + + optionsBuilder.ExecutionStrategy(d => new TestSqlServerRetryingExecutionStrategy(d)); + + optionsBuilder.CommandTimeout(SqlServerTestStore.CommandTimeout); + + return optionsBuilder; + } + + public static AzureSynapseDbContextOptionsBuilder ApplyConfiguration(this AzureSynapseDbContextOptionsBuilder optionsBuilder) + { + var maxBatch = TestEnvironment.GetInt(nameof(SqlServerDbContextOptionsBuilder.MaxBatchSize)); + if (maxBatch.HasValue) + { + optionsBuilder.MaxBatchSize(maxBatch.Value); + } + + optionsBuilder.UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery); + + optionsBuilder.ExecutionStrategy(d => new TestSqlServerRetryingExecutionStrategy(d)); + + optionsBuilder.CommandTimeout(SqlServerTestStore.CommandTimeout); + + return optionsBuilder; + } } diff --git a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerTestStore.cs b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerTestStore.cs index 226ef6fdbfd..ca1122a1b78 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerTestStore.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerTestStore.cs @@ -50,7 +50,7 @@ public static async Task CreateInitializedAsync( private readonly string? _initScript; private readonly string? _scriptPath; - private SqlServerTestStore( + protected SqlServerTestStore( string name, bool useFileName = false, bool? multipleActiveResultSets = null, From 0ca6bfa79eae4efe27a18fd2d747186d14414f7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jiri=20Cincura=20=E2=86=B9?= Date: Thu, 22 Aug 2024 11:55:31 +0200 Subject: [PATCH 2/2] Use different store names for funky data tests (#34506) --- .../Query/FunkyDataQueryAzureSynapseTest.cs | 3 +++ .../Query/FunkyDataQuerySqlServerTest.cs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQueryAzureSynapseTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQueryAzureSynapseTest.cs index 1101ad3c9d1..fbd05867453 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQueryAzureSynapseTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQueryAzureSynapseTest.cs @@ -595,5 +595,8 @@ public TestSqlLoggerFactory TestSqlLoggerFactory protected override ITestStoreFactory TestStoreFactory => AzureSynapseTestStoreFactory.Instance; + + protected override string StoreName + => nameof(FunkyDataQueryAzureSynapseTest); } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQuerySqlServerTest.cs index 1b1a68b03c2..f469d571141 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQuerySqlServerTest.cs @@ -596,5 +596,8 @@ public TestSqlLoggerFactory TestSqlLoggerFactory protected override ITestStoreFactory TestStoreFactory => SqlServerTestStoreFactory.Instance; + + protected override string StoreName + => nameof(FunkyDataQuerySqlServerTest); } }