From 99a4dd8f026ecf926f5f396a0cbb45e5f7841dd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0lker=20amca?= Date: Wed, 23 Sep 2026 18:54:37 +0300 Subject: [PATCH 1/2] Escape LIKE wildcard chars correctly in SQL Server StartsWith(char) When string.StartsWith(char) is called with a LIKE wildcard ('%', '_' or '['), the SQL Server translator built the pattern with `LikeEscapeChar + s + "%"`. Both operands of the first `+` are chars, so they are added numerically and the pattern becomes e.g. `LIKE N'129%'` instead of `LIKE N'\%%' ESCAPE N'\'`. The query runs but matches the wrong rows. EndsWith and Contains were already concatenating correctly. Use string interpolation for both the constant and the parameter path, and add FunkyData tests covering the constant, parameter, bracket and negated cases. Fixes #38923 Co-Authored-By: Claude Fable 5.1 --- ...qlServerSqlTranslatingExpressionVisitor.cs | 4 +- .../Query/FunkyDataQueryTestBase.cs | 63 ++++++++++++++ .../Query/FunkyDataQuerySqlServerTest.cs | 82 +++++++++++++++++++ 3 files changed, 147 insertions(+), 2 deletions(-) diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs index b62e3ba5c11..80d81e00c46 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs @@ -578,7 +578,7 @@ string when _sqlServerSingletonOptions.EngineType is SqlServerEngineType.AzureSy _sqlExpressionFactory.Constant( methodType switch { - StartsEndsWithContains.StartsWith => LikeEscapeChar + s + "%", + StartsEndsWithContains.StartsWith => $"{LikeEscapeChar}{s}%", StartsEndsWithContains.EndsWith => "%" + LikeEscapeChar + s, StartsEndsWithContains.Contains => $"%{LikeEscapeChar}{s}%", @@ -762,7 +762,7 @@ SqlParameterExpression CreateRuntimeParameter( char s => methodType switch { - StartsEndsWithContains.StartsWith => LikeEscapeChar + s + "%", + StartsEndsWithContains.StartsWith => $"{LikeEscapeChar}{s}%", StartsEndsWithContains.EndsWith => "%" + LikeEscapeChar + s, StartsEndsWithContains.Contains => $"%{LikeEscapeChar}{s}%", _ => throw new ArgumentOutOfRangeException(nameof(methodType), methodType, null) diff --git a/test/EFCore.Specification.Tests/Query/FunkyDataQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/FunkyDataQueryTestBase.cs index 57a78779076..11817519f8e 100644 --- a/test/EFCore.Specification.Tests/Query/FunkyDataQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/FunkyDataQueryTestBase.cs @@ -242,6 +242,69 @@ await AssertQuery( ss => ss.Set().Where(c => true).Select(c => c.FirstName)); } + [Theory, MemberData(nameof(IsAsyncData))] + public virtual async Task String_starts_with_on_argument_with_wildcard_char_constant(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(c => c.FirstName!.StartsWith('%')).Select(c => c.FirstName), + ss => ss.Set().Where(c => c.FirstName!.MaybeScalar(x => x.StartsWith('%')) == true).Select(c => c.FirstName)); + + await AssertQuery( + async, + ss => ss.Set().Where(c => c.FirstName!.StartsWith('_')).Select(c => c.FirstName), + ss => ss.Set().Where(c => c.FirstName!.MaybeScalar(x => x.StartsWith('_')) == true).Select(c => c.FirstName)); + + await AssertQuery( + async, + ss => ss.Set().Where(c => c.FirstName!.StartsWith('[')).Select(c => c.FirstName), + ss => ss.Set().Where(c => c.FirstName!.MaybeScalar(x => x.StartsWith('[')) == true).Select(c => c.FirstName)); + + await AssertQuery( + async, + ss => ss.Set().Where(c => c.FirstName!.StartsWith('B')).Select(c => c.FirstName), + ss => ss.Set().Where(c => c.FirstName!.MaybeScalar(x => x.StartsWith('B')) == true).Select(c => c.FirstName)); + + await AssertQuery( + async, + ss => ss.Set().Where(c => !c.FirstName!.StartsWith('_')).Select(c => c.FirstName), + ss => ss.Set().Where(c => c.FirstName!.MaybeScalar(x => x.StartsWith('_')) != true).Select(c => c.FirstName)); + } + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual async Task String_starts_with_on_argument_with_wildcard_char_parameter(bool async) + { + var prm1 = '%'; + await AssertQuery( + async, + ss => ss.Set().Where(c => c.FirstName!.StartsWith(prm1)).Select(c => c.FirstName), + ss => ss.Set().Where(c => c.FirstName!.MaybeScalar(x => x.StartsWith(prm1)) == true).Select(c => c.FirstName)); + + var prm2 = '_'; + await AssertQuery( + async, + ss => ss.Set().Where(c => c.FirstName!.StartsWith(prm2)).Select(c => c.FirstName), + ss => ss.Set().Where(c => c.FirstName!.MaybeScalar(x => x.StartsWith(prm2)) == true).Select(c => c.FirstName)); + + var prm3 = '['; + await AssertQuery( + async, + ss => ss.Set().Where(c => c.FirstName!.StartsWith(prm3)).Select(c => c.FirstName), + ss => ss.Set().Where(c => c.FirstName!.MaybeScalar(x => x.StartsWith(prm3)) == true).Select(c => c.FirstName)); + + var prm4 = 'B'; + await AssertQuery( + async, + ss => ss.Set().Where(c => c.FirstName!.StartsWith(prm4)).Select(c => c.FirstName), + ss => ss.Set().Where(c => c.FirstName!.MaybeScalar(x => x.StartsWith(prm4)) == true).Select(c => c.FirstName)); + + var prm5 = '_'; + await AssertQuery( + async, + ss => ss.Set().Where(c => !c.FirstName!.StartsWith(prm5)).Select(c => c.FirstName), + ss => ss.Set().Where(c => c.FirstName!.MaybeScalar(x => x.StartsWith(prm5)) != true).Select(c => c.FirstName)); + } + [Theory, MemberData(nameof(IsAsyncData))] public virtual async Task String_starts_with_on_argument_with_bracket(bool async) { diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQuerySqlServerTest.cs index f9ab13a3d98..a75006b30da 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQuerySqlServerTest.cs @@ -272,6 +272,88 @@ FROM [FunkyCustomers] AS [f] """); } + public override async Task String_starts_with_on_argument_with_wildcard_char_constant(bool async) + { + await base.String_starts_with_on_argument_with_wildcard_char_constant(async); + + AssertSql( + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] LIKE N'\%%' ESCAPE N'\' +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] LIKE N'\_%' ESCAPE N'\' +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] LIKE N'\[%' ESCAPE N'\' +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] LIKE N'B%' +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] NOT LIKE N'\_%' ESCAPE N'\' OR [f].[FirstName] IS NULL +"""); + } + + public override async Task String_starts_with_on_argument_with_wildcard_char_parameter(bool async) + { + await base.String_starts_with_on_argument_with_wildcard_char_parameter(async); + + AssertSql( + """ +@prm1_startswith='\%%' (Size = 4000) + +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] LIKE @prm1_startswith ESCAPE N'\' +""", + // + """ +@prm2_startswith='\_%' (Size = 4000) + +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] LIKE @prm2_startswith ESCAPE N'\' +""", + // + """ +@prm3_startswith='\[%' (Size = 4000) + +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] LIKE @prm3_startswith ESCAPE N'\' +""", + // + """ +@prm4_startswith='B%' (Size = 4000) + +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] LIKE @prm4_startswith ESCAPE N'\' +""", + // + """ +@prm5_startswith='\_%' (Size = 4000) + +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] NOT LIKE @prm5_startswith ESCAPE N'\' OR [f].[FirstName] IS NULL +"""); + } + public override async Task String_starts_with_on_argument_with_bracket(bool async) { await base.String_starts_with_on_argument_with_bracket(async); From 838bb57d8b4eb056f218cfdb93ad1eb5c96e5e53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0lker=20amca?= Date: Sat, 26 Sep 2026 12:31:42 +0300 Subject: [PATCH 2/2] Use interpolation for all LIKE patterns and skip ESCAPE for char wildcards on Azure Synapse Rewrite every StartsWith/EndsWith/Contains LIKE pattern arm in the SQL Server and SQLite translators as an interpolated string, so no arm relies on operand order to keep a char from being added numerically. On Azure Synapse the constant char wildcard case still emitted LIKE ... ESCAPE, which Synapse does not support. Route it through TranslateWithoutLike like the string constant and parameter cases already do, passing a string constant so the LEFT/RIGHT/CHARINDEX translation does not render a char-valued constant with a string type mapping. Add Synapse baselines for the char wildcard tests. Co-Authored-By: Claude Fable 5.1 --- ...qlServerSqlTranslatingExpressionVisitor.cs | 47 ++++++----- .../SqliteSqlTranslatingExpressionVisitor.cs | 14 ++-- .../Query/FunkyDataQueryAzureSynapseTest.cs | 82 +++++++++++++++++++ 3 files changed, 117 insertions(+), 26 deletions(-) diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs index 80d81e00c46..57f5d2ad8a4 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs @@ -535,8 +535,8 @@ Expression TranslateStartsEndsWithContains(Expression instance, Expression patte _sqlExpressionFactory.Constant( methodType switch { - StartsEndsWithContains.StartsWith => s + '%', - StartsEndsWithContains.EndsWith => '%' + s, + StartsEndsWithContains.StartsWith => $"{s}%", + StartsEndsWithContains.EndsWith => $"%{s}", StartsEndsWithContains.Contains => $"%{s}%", _ => throw new ArgumentOutOfRangeException(nameof(methodType), methodType, null) @@ -552,8 +552,8 @@ string when _sqlServerSingletonOptions.EngineType is SqlServerEngineType.AzureSy _sqlExpressionFactory.Constant( methodType switch { - StartsEndsWithContains.StartsWith => EscapeLikePattern(s) + '%', - StartsEndsWithContains.EndsWith => '%' + EscapeLikePattern(s), + StartsEndsWithContains.StartsWith => $"{EscapeLikePattern(s)}%", + StartsEndsWithContains.EndsWith => $"%{EscapeLikePattern(s)}", StartsEndsWithContains.Contains => $"%{EscapeLikePattern(s)}%", _ => throw new ArgumentOutOfRangeException(nameof(methodType), methodType, null) @@ -566,20 +566,27 @@ string when _sqlServerSingletonOptions.EngineType is SqlServerEngineType.AzureSy _sqlExpressionFactory.Constant( methodType switch { - StartsEndsWithContains.StartsWith => s + "%", - StartsEndsWithContains.EndsWith => "%" + s, + StartsEndsWithContains.StartsWith => $"{s}%", + StartsEndsWithContains.EndsWith => $"%{s}", StartsEndsWithContains.Contains => $"%{s}%", _ => throw new ArgumentOutOfRangeException(nameof(methodType), methodType, null) })), + // Azure Synapse does not support ESCAPE clause in LIKE + // fallback to translation like with column/expression + char s when _sqlServerSingletonOptions.EngineType is SqlServerEngineType.AzureSynapse + => TranslateWithoutLike( + patternIsNonEmptyConstantString: true, + pattern: _sqlExpressionFactory.Constant(s.ToString(), stringTypeMapping)), + char s => _sqlExpressionFactory.Like( translatedInstance, _sqlExpressionFactory.Constant( methodType switch { StartsEndsWithContains.StartsWith => $"{LikeEscapeChar}{s}%", - StartsEndsWithContains.EndsWith => "%" + LikeEscapeChar + s, + StartsEndsWithContains.EndsWith => $"%{LikeEscapeChar}{s}", StartsEndsWithContains.Contains => $"%{LikeEscapeChar}{s}%", _ => throw new ArgumentOutOfRangeException(nameof(methodType), methodType, null) @@ -621,8 +628,10 @@ string when _sqlServerSingletonOptions.EngineType is SqlServerEngineType.AzureSy return TranslateWithoutLike(); } - SqlExpression TranslateWithoutLike(bool patternIsNonEmptyConstantString = false) + SqlExpression TranslateWithoutLike(bool patternIsNonEmptyConstantString = false, SqlExpression? pattern = null) { + pattern ??= translatedPattern; + return methodType switch { // For StartsWith/EndsWith, use LEFT or RIGHT instead to extract substring and compare: @@ -634,7 +643,7 @@ StartsEndsWithContains.StartsWith or StartsEndsWithContains.EndsWith => _sqlExpressionFactory.AndAlso( _sqlExpressionFactory.IsNotNull(translatedInstance), _sqlExpressionFactory.AndAlso( - _sqlExpressionFactory.IsNotNull(translatedPattern), + _sqlExpressionFactory.IsNotNull(pattern), _sqlExpressionFactory.Equal( _sqlExpressionFactory.Function( methodType is StartsEndsWithContains.StartsWith ? "LEFT" : "RIGHT", @@ -642,7 +651,7 @@ StartsEndsWithContains.StartsWith or StartsEndsWithContains.EndsWith translatedInstance, _sqlExpressionFactory.Function( "LEN", - [translatedPattern], + [pattern], nullable: true, argumentsPropagateNullability: Statics.TrueArrays[1], typeof(int)) @@ -651,7 +660,7 @@ StartsEndsWithContains.StartsWith or StartsEndsWithContains.EndsWith argumentsPropagateNullability: Statics.TrueArrays[2], typeof(string), stringTypeMapping), - translatedPattern))), + pattern))), // For Contains, just use CHARINDEX and check if the result is greater than 0. StartsEndsWithContains.Contains when patternIsNonEmptyConstantString @@ -665,11 +674,11 @@ StartsEndsWithContains.Contains when patternIsNonEmptyConstantString => _sqlExpressionFactory.AndAlso( _sqlExpressionFactory.IsNotNull(translatedInstance), _sqlExpressionFactory.AndAlso( - _sqlExpressionFactory.IsNotNull(translatedPattern), + _sqlExpressionFactory.IsNotNull(pattern), _sqlExpressionFactory.OrElse( CharIndexGreaterThanZero(), _sqlExpressionFactory.Like( - translatedPattern, + pattern, _sqlExpressionFactory.Constant(string.Empty, stringTypeMapping))))), _ => throw new UnreachableException() @@ -679,7 +688,7 @@ SqlExpression CharIndexGreaterThanZero() => _sqlExpressionFactory.GreaterThan( _sqlExpressionFactory.Function( "CHARINDEX", - [translatedPattern, translatedInstance], + [pattern, translatedInstance], nullable: true, argumentsPropagateNullability: Statics.TrueArrays[2], typeof(int)), @@ -746,16 +755,16 @@ SqlParameterExpression CreateRuntimeParameter( string s => methodType switch { - StartsEndsWithContains.StartsWith => EscapeLikePattern(s) + '%', - StartsEndsWithContains.EndsWith => '%' + EscapeLikePattern(s), + StartsEndsWithContains.StartsWith => $"{EscapeLikePattern(s)}%", + StartsEndsWithContains.EndsWith => $"%{EscapeLikePattern(s)}", StartsEndsWithContains.Contains => $"%{EscapeLikePattern(s)}%", _ => throw new ArgumentOutOfRangeException(nameof(methodType), methodType, null) }, char s when !IsLikeWildChar(s) => methodType switch { - StartsEndsWithContains.StartsWith => s + "%", - StartsEndsWithContains.EndsWith => "%" + s, + StartsEndsWithContains.StartsWith => $"{s}%", + StartsEndsWithContains.EndsWith => $"%{s}", StartsEndsWithContains.Contains => $"%{s}%", _ => throw new ArgumentOutOfRangeException(nameof(methodType), methodType, null) }, @@ -763,7 +772,7 @@ SqlParameterExpression CreateRuntimeParameter( char s => methodType switch { StartsEndsWithContains.StartsWith => $"{LikeEscapeChar}{s}%", - StartsEndsWithContains.EndsWith => "%" + LikeEscapeChar + s, + StartsEndsWithContains.EndsWith => $"%{LikeEscapeChar}{s}", StartsEndsWithContains.Contains => $"%{LikeEscapeChar}{s}%", _ => throw new ArgumentOutOfRangeException(nameof(methodType), methodType, null) }, diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlTranslatingExpressionVisitor.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlTranslatingExpressionVisitor.cs index b595ff3eb43..62fa285c7f7 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlTranslatingExpressionVisitor.cs @@ -305,20 +305,20 @@ Expression TranslateStartsEndsWith(Expression instance, Expression pattern, bool string s => s.Any(IsLikeWildChar) ? _sqlExpressionFactory.Like( translatedInstance, - _sqlExpressionFactory.Constant(startsWith ? EscapeLikePattern(s) + '%' : '%' + EscapeLikePattern(s)), + _sqlExpressionFactory.Constant(startsWith ? $"{EscapeLikePattern(s)}%" : $"%{EscapeLikePattern(s)}"), _sqlExpressionFactory.Constant(LikeEscapeString)) : _sqlExpressionFactory.Like( translatedInstance, - _sqlExpressionFactory.Constant(startsWith ? s + '%' : '%' + s)), + _sqlExpressionFactory.Constant(startsWith ? $"{s}%" : $"%{s}")), char s => IsLikeWildChar(s) ? _sqlExpressionFactory.Like( translatedInstance, - _sqlExpressionFactory.Constant(startsWith ? LikeEscapeString + s + "%" : '%' + LikeEscapeString + s), + _sqlExpressionFactory.Constant(startsWith ? $"{LikeEscapeChar}{s}%" : $"%{LikeEscapeChar}{s}"), _sqlExpressionFactory.Constant(LikeEscapeString)) : _sqlExpressionFactory.Like( translatedInstance, - _sqlExpressionFactory.Constant(startsWith ? s + "%" : "%" + s)), + _sqlExpressionFactory.Constant(startsWith ? $"{s}%" : $"%{s}")), _ => throw new UnreachableException() }; @@ -436,11 +436,11 @@ Expression TranslateStartsEndsWith(Expression instance, Expression pattern, bool // Return % which always matches instead. "" => "%", - string s => startsWith ? EscapeLikePattern(s) + '%' : '%' + EscapeLikePattern(s), + string s => startsWith ? $"{EscapeLikePattern(s)}%" : $"%{EscapeLikePattern(s)}", - char s when IsLikeWildChar(s) => startsWith ? LikeEscapeString + s + '%' : '%' + LikeEscapeString + s, + char s when IsLikeWildChar(s) => startsWith ? $"{LikeEscapeChar}{s}%" : $"%{LikeEscapeChar}{s}", - char s => startsWith ? s + "%" : "%" + s, + char s => startsWith ? $"{s}%" : $"%{s}", _ => throw new UnreachableException() }; diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQueryAzureSynapseTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQueryAzureSynapseTest.cs index 77ad6088443..a9bf35f73e7 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQueryAzureSynapseTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQueryAzureSynapseTest.cs @@ -273,6 +273,88 @@ FROM [FunkyCustomers] AS [f] """); } + public override async Task String_starts_with_on_argument_with_wildcard_char_constant(bool async) + { + await base.String_starts_with_on_argument_with_wildcard_char_constant(async); + + AssertSql( + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND LEFT([f].[FirstName], LEN(N'%')) = N'%' +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND LEFT([f].[FirstName], LEN(N'_')) = N'_' +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND LEFT([f].[FirstName], LEN(N'[')) = N'[' +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] LIKE N'B%' +""", + // + """ +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NULL OR LEFT([f].[FirstName], LEN(N'_')) <> N'_' +"""); + } + + public override async Task String_starts_with_on_argument_with_wildcard_char_parameter(bool async) + { + await base.String_starts_with_on_argument_with_wildcard_char_parameter(async); + + AssertSql( + """ +@prm1='%' (Size = -1) (DbType = String) + +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND LEFT([f].[FirstName], LEN(@prm1)) = @prm1 +""", + // + """ +@prm2='_' (Size = -1) (DbType = String) + +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND LEFT([f].[FirstName], LEN(@prm2)) = @prm2 +""", + // + """ +@prm3='[' (Size = -1) (DbType = String) + +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND LEFT([f].[FirstName], LEN(@prm3)) = @prm3 +""", + // + """ +@prm4='B' (Size = -1) (DbType = String) + +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NOT NULL AND LEFT([f].[FirstName], LEN(@prm4)) = @prm4 +""", + // + """ +@prm5='_' (Size = -1) (DbType = String) + +SELECT [f].[FirstName] +FROM [FunkyCustomers] AS [f] +WHERE [f].[FirstName] IS NULL OR LEFT([f].[FirstName], LEN(@prm5)) <> @prm5 +"""); + } + public override async Task String_starts_with_on_argument_with_bracket(bool async) { await base.String_starts_with_on_argument_with_bracket(async);