Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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.StartsWith => $"{LikeEscapeChar}{s}%",
Comment thread
AndriySvyryd marked this conversation as resolved.
Comment thread
AndriySvyryd marked this conversation as resolved.
StartsEndsWithContains.EndsWith => $"%{LikeEscapeChar}{s}",
StartsEndsWithContains.Contains => $"%{LikeEscapeChar}{s}%",

_ => throw new ArgumentOutOfRangeException(nameof(methodType), methodType, null)
Expand Down Expand Up @@ -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:
Expand All @@ -634,15 +643,15 @@ 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",
[
translatedInstance,
_sqlExpressionFactory.Function(
"LEN",
[translatedPattern],
[pattern],
nullable: true,
argumentsPropagateNullability: Statics.TrueArrays[1],
typeof(int))
Expand All @@ -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
Expand All @@ -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()
Expand All @@ -679,7 +688,7 @@ SqlExpression CharIndexGreaterThanZero()
=> _sqlExpressionFactory.GreaterThan(
_sqlExpressionFactory.Function(
"CHARINDEX",
[translatedPattern, translatedInstance],
[pattern, translatedInstance],
nullable: true,
argumentsPropagateNullability: Statics.TrueArrays[2],
typeof(int)),
Expand Down Expand Up @@ -746,24 +755,24 @@ 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)
},

char s => methodType switch
{
StartsEndsWithContains.StartsWith => LikeEscapeChar + s + "%",
StartsEndsWithContains.EndsWith => "%" + LikeEscapeChar + s,
StartsEndsWithContains.StartsWith => $"{LikeEscapeChar}{s}%",
StartsEndsWithContains.EndsWith => $"%{LikeEscapeChar}{s}",
StartsEndsWithContains.Contains => $"%{LikeEscapeChar}{s}%",
_ => throw new ArgumentOutOfRangeException(nameof(methodType), methodType, null)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
};
Expand Down Expand Up @@ -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()
};
Expand Down
63 changes: 63 additions & 0 deletions test/EFCore.Specification.Tests/Query/FunkyDataQueryTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,69 @@ await AssertQuery(
ss => ss.Set<FunkyCustomer>().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,
Comment thread
AndriySvyryd marked this conversation as resolved.
ss => ss.Set<FunkyCustomer>().Where(c => c.FirstName!.StartsWith('%')).Select(c => c.FirstName),
ss => ss.Set<FunkyCustomer>().Where(c => c.FirstName!.MaybeScalar(x => x.StartsWith('%')) == true).Select(c => c.FirstName));

await AssertQuery(
async,
ss => ss.Set<FunkyCustomer>().Where(c => c.FirstName!.StartsWith('_')).Select(c => c.FirstName),
ss => ss.Set<FunkyCustomer>().Where(c => c.FirstName!.MaybeScalar(x => x.StartsWith('_')) == true).Select(c => c.FirstName));

await AssertQuery(
async,
ss => ss.Set<FunkyCustomer>().Where(c => c.FirstName!.StartsWith('[')).Select(c => c.FirstName),
ss => ss.Set<FunkyCustomer>().Where(c => c.FirstName!.MaybeScalar(x => x.StartsWith('[')) == true).Select(c => c.FirstName));

await AssertQuery(
async,
ss => ss.Set<FunkyCustomer>().Where(c => c.FirstName!.StartsWith('B')).Select(c => c.FirstName),
ss => ss.Set<FunkyCustomer>().Where(c => c.FirstName!.MaybeScalar(x => x.StartsWith('B')) == true).Select(c => c.FirstName));

await AssertQuery(
async,
ss => ss.Set<FunkyCustomer>().Where(c => !c.FirstName!.StartsWith('_')).Select(c => c.FirstName),
ss => ss.Set<FunkyCustomer>().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<FunkyCustomer>().Where(c => c.FirstName!.StartsWith(prm1)).Select(c => c.FirstName),
ss => ss.Set<FunkyCustomer>().Where(c => c.FirstName!.MaybeScalar(x => x.StartsWith(prm1)) == true).Select(c => c.FirstName));

var prm2 = '_';
await AssertQuery(
async,
ss => ss.Set<FunkyCustomer>().Where(c => c.FirstName!.StartsWith(prm2)).Select(c => c.FirstName),
ss => ss.Set<FunkyCustomer>().Where(c => c.FirstName!.MaybeScalar(x => x.StartsWith(prm2)) == true).Select(c => c.FirstName));

var prm3 = '[';
await AssertQuery(
async,
ss => ss.Set<FunkyCustomer>().Where(c => c.FirstName!.StartsWith(prm3)).Select(c => c.FirstName),
ss => ss.Set<FunkyCustomer>().Where(c => c.FirstName!.MaybeScalar(x => x.StartsWith(prm3)) == true).Select(c => c.FirstName));

var prm4 = 'B';
await AssertQuery(
async,
ss => ss.Set<FunkyCustomer>().Where(c => c.FirstName!.StartsWith(prm4)).Select(c => c.FirstName),
ss => ss.Set<FunkyCustomer>().Where(c => c.FirstName!.MaybeScalar(x => x.StartsWith(prm4)) == true).Select(c => c.FirstName));

var prm5 = '_';
await AssertQuery(
async,
ss => ss.Set<FunkyCustomer>().Where(c => !c.FirstName!.StartsWith(prm5)).Select(c => c.FirstName),
ss => ss.Set<FunkyCustomer>().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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading
Loading