Skip to content

Commit ca3abee

Browse files
authored
Fix ESCAPE clause for Azure Synapse. (#34463)
1 parent 80a01cd commit ca3abee

File tree

8 files changed

+821
-78
lines changed

8 files changed

+821
-78
lines changed

src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs

Lines changed: 85 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -297,20 +297,8 @@ bool TryTranslateStartsEndsWithContains(
297297
// (but SqlNullabilityProcess will convert this to a true constant if the instance is non-nullable)
298298
"" => _sqlExpressionFactory.Like(translatedInstance, _sqlExpressionFactory.Constant("%")),
299299

300-
string s => s.Any(IsLikeWildChar)
301-
? _sqlExpressionFactory.Like(
302-
translatedInstance,
303-
_sqlExpressionFactory.Constant(
304-
methodType switch
305-
{
306-
StartsEndsWithContains.StartsWith => EscapeLikePattern(s) + '%',
307-
StartsEndsWithContains.EndsWith => '%' + EscapeLikePattern(s),
308-
StartsEndsWithContains.Contains => $"%{EscapeLikePattern(s)}%",
309-
310-
_ => throw new ArgumentOutOfRangeException(nameof(methodType), methodType, null)
311-
}),
312-
_sqlExpressionFactory.Constant(LikeEscapeString))
313-
: _sqlExpressionFactory.Like(
300+
string s when !s.Any(IsLikeWildChar)
301+
=> _sqlExpressionFactory.Like(
314302
translatedInstance,
315303
_sqlExpressionFactory.Constant(
316304
methodType switch
@@ -322,14 +310,35 @@ bool TryTranslateStartsEndsWithContains(
322310
_ => throw new ArgumentOutOfRangeException(nameof(methodType), methodType, null)
323311
})),
324312

313+
// Azure Synapse does not support ESCAPE clause in LIKE
314+
// fallback to translation like with column/expression
315+
string s when _sqlServerSingletonOptions.EngineType == SqlServerEngineType.AzureSynapse
316+
=> TranslateWithoutLike(patternIsNonEmptyConstantString: true),
317+
318+
string s => _sqlExpressionFactory.Like(
319+
translatedInstance,
320+
_sqlExpressionFactory.Constant(
321+
methodType switch
322+
{
323+
StartsEndsWithContains.StartsWith => EscapeLikePattern(s) + '%',
324+
StartsEndsWithContains.EndsWith => '%' + EscapeLikePattern(s),
325+
StartsEndsWithContains.Contains => $"%{EscapeLikePattern(s)}%",
326+
327+
_ => throw new ArgumentOutOfRangeException(nameof(methodType), methodType, null)
328+
}),
329+
_sqlExpressionFactory.Constant(LikeEscapeString)),
330+
325331
_ => throw new UnreachableException()
326332
};
327333

328334
return true;
329335
}
330336

331337
case SqlParameterExpression patternParameter
332-
when patternParameter.Name.StartsWith(QueryCompilationContext.QueryParameterPrefix, StringComparison.Ordinal):
338+
when patternParameter.Name.StartsWith(QueryCompilationContext.QueryParameterPrefix, StringComparison.Ordinal)
339+
// Azure Synapse does not support ESCAPE clause in LIKE
340+
// fall through to translation like with column/expression
341+
&& _sqlServerSingletonOptions.EngineType != SqlServerEngineType.AzureSynapse:
333342
{
334343
// The pattern is a parameter, register a runtime parameter that will contain the rewritten LIKE pattern, where
335344
// all special characters have been escaped.
@@ -356,61 +365,74 @@ when patternParameter.Name.StartsWith(QueryCompilationContext.QueryParameterPref
356365
default:
357366
// The pattern is a column or a complex expression; the possible special characters in the pattern cannot be escaped,
358367
// preventing us from translating to LIKE.
359-
translation = methodType switch
360-
{
361-
// For StartsWith/EndsWith, use LEFT or RIGHT instead to extract substring and compare:
362-
// WHERE instance IS NOT NULL AND pattern IS NOT NULL AND LEFT(instance, LEN(pattern)) = pattern
363-
// This is less efficient than LIKE (i.e. StartsWith does an index scan instead of seek), but we have no choice.
364-
// Note that we compensate for the case where both the instance and the pattern are null (null.StartsWith(null)); a
365-
// simple equality would yield true in that case, but we want false. We technically
366-
StartsEndsWithContains.StartsWith or StartsEndsWithContains.EndsWith
367-
=> _sqlExpressionFactory.AndAlso(
368-
_sqlExpressionFactory.IsNotNull(translatedInstance),
369-
_sqlExpressionFactory.AndAlso(
370-
_sqlExpressionFactory.IsNotNull(translatedPattern),
371-
_sqlExpressionFactory.Equal(
372-
_sqlExpressionFactory.Function(
373-
methodType is StartsEndsWithContains.StartsWith ? "LEFT" : "RIGHT",
374-
new[]
375-
{
368+
translation = TranslateWithoutLike();
369+
return true;
370+
}
371+
372+
SqlExpression TranslateWithoutLike(bool patternIsNonEmptyConstantString = false)
373+
{
374+
return methodType switch
375+
{
376+
// For StartsWith/EndsWith, use LEFT or RIGHT instead to extract substring and compare:
377+
// WHERE instance IS NOT NULL AND pattern IS NOT NULL AND LEFT(instance, LEN(pattern)) = pattern
378+
// This is less efficient than LIKE (i.e. StartsWith does an index scan instead of seek), but we have no choice.
379+
// Note that we compensate for the case where both the instance and the pattern are null (null.StartsWith(null)); a
380+
// simple equality would yield true in that case, but we want false. We technically
381+
StartsEndsWithContains.StartsWith or StartsEndsWithContains.EndsWith
382+
=> _sqlExpressionFactory.AndAlso(
383+
_sqlExpressionFactory.IsNotNull(translatedInstance),
384+
_sqlExpressionFactory.AndAlso(
385+
_sqlExpressionFactory.IsNotNull(translatedPattern),
386+
_sqlExpressionFactory.Equal(
387+
_sqlExpressionFactory.Function(
388+
methodType is StartsEndsWithContains.StartsWith ? "LEFT" : "RIGHT",
389+
new[]
390+
{
376391
translatedInstance,
377392
_sqlExpressionFactory.Function(
378393
"LEN",
379394
new[] { translatedPattern },
380395
nullable: true,
381396
argumentsPropagateNullability: new[] { true },
382397
typeof(int))
383-
},
384-
nullable: true,
385-
argumentsPropagateNullability: new[] { true, true },
386-
typeof(string),
387-
stringTypeMapping),
388-
translatedPattern))),
389-
390-
// For Contains, just use CHARINDEX and check if the result is greater than 0.
391-
// Add a check to return null when the pattern is an empty string (and the string isn't null)
392-
StartsEndsWithContains.Contains
393-
=> _sqlExpressionFactory.AndAlso(
394-
_sqlExpressionFactory.IsNotNull(translatedInstance),
395-
_sqlExpressionFactory.AndAlso(
396-
_sqlExpressionFactory.IsNotNull(translatedPattern),
397-
_sqlExpressionFactory.OrElse(
398-
_sqlExpressionFactory.GreaterThan(
399-
_sqlExpressionFactory.Function(
400-
"CHARINDEX",
401-
new[] { translatedPattern, translatedInstance },
402-
nullable: true,
403-
argumentsPropagateNullability: new[] { true, true },
404-
typeof(int)),
405-
_sqlExpressionFactory.Constant(0)),
406-
_sqlExpressionFactory.Like(
407-
translatedPattern,
408-
_sqlExpressionFactory.Constant(string.Empty, stringTypeMapping))))),
409-
410-
_ => throw new UnreachableException()
411-
};
412-
413-
return true;
398+
},
399+
nullable: true,
400+
argumentsPropagateNullability: new[] { true, true },
401+
typeof(string),
402+
stringTypeMapping),
403+
translatedPattern))),
404+
405+
// For Contains, just use CHARINDEX and check if the result is greater than 0.
406+
StartsEndsWithContains.Contains when patternIsNonEmptyConstantString
407+
=> _sqlExpressionFactory.AndAlso(
408+
_sqlExpressionFactory.IsNotNull(translatedInstance),
409+
CharIndexGreaterThanZero()),
410+
411+
// For Contains, just use CHARINDEX and check if the result is greater than 0.
412+
// Add a check to return null when the pattern is an empty string (and the string isn't null)
413+
StartsEndsWithContains.Contains
414+
=> _sqlExpressionFactory.AndAlso(
415+
_sqlExpressionFactory.IsNotNull(translatedInstance),
416+
_sqlExpressionFactory.AndAlso(
417+
_sqlExpressionFactory.IsNotNull(translatedPattern),
418+
_sqlExpressionFactory.OrElse(
419+
CharIndexGreaterThanZero(),
420+
_sqlExpressionFactory.Like(
421+
translatedPattern,
422+
_sqlExpressionFactory.Constant(string.Empty, stringTypeMapping))))),
423+
424+
_ => throw new UnreachableException()
425+
};
426+
427+
SqlExpression CharIndexGreaterThanZero()
428+
=> _sqlExpressionFactory.GreaterThan(
429+
_sqlExpressionFactory.Function(
430+
"CHARINDEX",
431+
new[] { translatedPattern, translatedInstance },
432+
nullable: true,
433+
argumentsPropagateNullability: new[] { true, true },
434+
typeof(int)),
435+
_sqlExpressionFactory.Constant(0));
414436
}
415437
}
416438
}

0 commit comments

Comments
 (0)