Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(RC2) Fix ESCAPE clause for Azure Synapse. (#34463) #34509

Merged
merged 2 commits into from
Aug 22, 2024
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 @@ -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
Expand All @@ -322,14 +310,35 @@ 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()
};

return true;
}

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.
Expand All @@ -356,61 +365,74 @@ 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",
new[] { translatedPattern },
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));
}
}
}
Expand Down
Loading