Skip to content

Commit

Permalink
TrimStart and TrimEnd with optional char array implementation for SQL…
Browse files Browse the repository at this point in the history
… Server (#33715)

* TrimStart and TrimEnd with option char array implementation for SQL Server.

Fixes #22924

* Using SqlServerCondition.SupportsFunctions2022 for LTRIM and RTRIM. Handling compatibility level to low.

* Revert "Using SqlServerCondition.SupportsFunctions2022 for LTRIM and RTRIM. Handling compatibility level to low."

This reverts commit e1a73e7.

* Rolling back throwing exceptions gracefully. Processing trim start and end in a consistent way.

* Using StringBuilder instead of List. Other minor fixes.

* Only TrimStart and TrimEnd with the optional char or array must be validated by compatibility level.

* Fix method matching and cleanup

---------

Co-authored-by: acarrazana <acarrazana@easyworkforce.com>
Co-authored-by: Shay Rojansky <roji@roji.org>
  • Loading branch information
3 people authored Jun 20, 2024
1 parent c0c11c1 commit 9e15699
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 41 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal;

namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;

/// <summary>
Expand All @@ -17,7 +19,7 @@ public class SqlServerMethodCallTranslatorProvider : RelationalMethodCallTransla
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public SqlServerMethodCallTranslatorProvider(RelationalMethodCallTranslatorProviderDependencies dependencies)
public SqlServerMethodCallTranslatorProvider(RelationalMethodCallTranslatorProviderDependencies dependencies, ISqlServerSingletonOptions sqlServerSingletonOptions)
: base(dependencies)
{
var sqlExpressionFactory = dependencies.SqlExpressionFactory;
Expand All @@ -37,7 +39,7 @@ public SqlServerMethodCallTranslatorProvider(RelationalMethodCallTranslatorProvi
new SqlServerMathTranslator(sqlExpressionFactory),
new SqlServerNewGuidTranslator(sqlExpressionFactory),
new SqlServerObjectToStringTranslator(sqlExpressionFactory, typeMappingSource),
new SqlServerStringMethodTranslator(sqlExpressionFactory),
new SqlServerStringMethodTranslator(sqlExpressionFactory, sqlServerSingletonOptions),
new SqlServerTimeOnlyMethodTranslator(sqlExpressionFactory)
]);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal;
using ExpressionExtensions = Microsoft.EntityFrameworkCore.Query.ExpressionExtensions;

// ReSharper disable once CheckNamespace
Expand Down Expand Up @@ -62,6 +64,12 @@ private static readonly MethodInfo TrimEndMethodInfoWithCharArrayArg
private static readonly MethodInfo TrimMethodInfoWithCharArrayArg
= typeof(string).GetRuntimeMethod(nameof(string.Trim), [typeof(char[])])!;

private static readonly MethodInfo TrimStartMethodInfoWithCharArg
= typeof(string).GetRuntimeMethod(nameof(string.TrimStart), [typeof(char)])!;

private static readonly MethodInfo TrimEndMethodInfoWithCharArg
= typeof(string).GetRuntimeMethod(nameof(string.TrimEnd), [typeof(char)])!;

private static readonly MethodInfo FirstOrDefaultMethodInfoWithoutArgs
= typeof(Enumerable).GetRuntimeMethods().Single(
m => m.Name == nameof(Enumerable.FirstOrDefault)
Expand All @@ -79,15 +87,19 @@ private static readonly MethodInfo PatIndexMethodInfo

private readonly ISqlExpressionFactory _sqlExpressionFactory;

private readonly ISqlServerSingletonOptions _sqlServerSingletonOptions;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public SqlServerStringMethodTranslator(ISqlExpressionFactory sqlExpressionFactory)
public SqlServerStringMethodTranslator(ISqlExpressionFactory sqlExpressionFactory, ISqlServerSingletonOptions sqlServerSingletonOptions)
{
_sqlExpressionFactory = sqlExpressionFactory;

_sqlServerSingletonOptions = sqlServerSingletonOptions;
}

/// <summary>
Expand Down Expand Up @@ -186,38 +198,26 @@ public SqlServerStringMethodTranslator(ISqlExpressionFactory sqlExpressionFactor
instance.TypeMapping);
}

if (TrimStartMethodInfoWithoutArgs.Equals(method)
|| (TrimStartMethodInfoWithCharArrayArg.Equals(method)
// SqlServer LTRIM does not take arguments
&& ((arguments[0] as SqlConstantExpression)?.Value as Array)?.Length == 0))
// There's single-parameter LTRIM/RTRIM for all versions (trims whitespace), but startin with SQL Server 2022 there's also
// an overload that accepts the characters to trim.
if (method == TrimStartMethodInfoWithoutArgs
|| (method == TrimStartMethodInfoWithCharArrayArg && arguments[0] is SqlConstantExpression { Value: char[] { Length: 0 } })
|| (_sqlServerSingletonOptions.CompatibilityLevel >= 160
&& (method == TrimStartMethodInfoWithCharArg || method == TrimStartMethodInfoWithCharArrayArg)))
{
return _sqlExpressionFactory.Function(
"LTRIM",
new[] { instance },
nullable: true,
argumentsPropagateNullability: new[] { true },
instance.Type,
instance.TypeMapping);
return ProcessTrimStartEnd(instance, arguments, "LTRIM");
}

if (TrimEndMethodInfoWithoutArgs.Equals(method)
|| (TrimEndMethodInfoWithCharArrayArg.Equals(method)
// SqlServer RTRIM does not take arguments
&& ((arguments[0] as SqlConstantExpression)?.Value as Array)?.Length == 0))
if (method == TrimEndMethodInfoWithoutArgs
|| (method == TrimEndMethodInfoWithCharArrayArg && arguments[0] is SqlConstantExpression { Value: char[] { Length: 0 } })
|| (_sqlServerSingletonOptions.CompatibilityLevel >= 160
&& (method == TrimEndMethodInfoWithCharArg || method == TrimEndMethodInfoWithCharArrayArg)))
{
return _sqlExpressionFactory.Function(
"RTRIM",
new[] { instance },
nullable: true,
argumentsPropagateNullability: new[] { true },
instance.Type,
instance.TypeMapping);
return ProcessTrimStartEnd(instance, arguments, "RTRIM");
}

if (TrimMethodInfoWithoutArgs.Equals(method)
|| (TrimMethodInfoWithCharArrayArg.Equals(method)
// SqlServer LTRIM/RTRIM does not take arguments
&& ((arguments[0] as SqlConstantExpression)?.Value as Array)?.Length == 0))
if (method == TrimMethodInfoWithoutArgs
|| (method == TrimMethodInfoWithCharArrayArg && arguments[0] is SqlConstantExpression { Value: char[] { Length: 0 } }))
{
return _sqlExpressionFactory.Function(
"LTRIM",
Expand Down Expand Up @@ -381,4 +381,26 @@ private SqlExpression TranslateIndexOf(

return _sqlExpressionFactory.Subtract(charIndexExpression, offsetExpression);
}

private SqlExpression? ProcessTrimStartEnd(SqlExpression instance, IReadOnlyList<SqlExpression> arguments, string functionName)
{
SqlConstantExpression? charactersToTrim = null;
if (arguments.Count > 0 && arguments[0] is SqlConstantExpression { Value: var charactersToTrimValue })
{
charactersToTrim = charactersToTrimValue switch
{
char singleChar => _sqlExpressionFactory.Constant(singleChar.ToString(), instance.TypeMapping),
char[] charArray => _sqlExpressionFactory.Constant(new string(charArray), instance.TypeMapping),
_ => throw new UnreachableException("Invalid parameter type for string.TrimStart/TrimEnd")
};
}

return _sqlExpressionFactory.Function(
functionName,
arguments: charactersToTrim is null ? [instance] : [instance, charactersToTrim],
nullable: true,
argumentsPropagateNullability: charactersToTrim is null ? [true] : [true, true],
instance.Type,
instance.TypeMapping);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2656,20 +2656,30 @@ WHERE LTRIM([c].[ContactTitle]) = N'Owner'
""");
}

[SqlServerCondition(SqlServerCondition.SupportsFunctions2022)]
public override async Task TrimStart_with_char_argument_in_predicate(bool async)
{
// String.Trim with parameters. Issue #22927.
await AssertTranslationFailed(() => base.TrimStart_with_char_argument_in_predicate(async));
await base.TrimStart_with_char_argument_in_predicate(async);

AssertSql();
AssertSql(
"""
SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region]
FROM [Customers] AS [c]
WHERE LTRIM([c].[ContactTitle], N'O') = N'wner'
""");
}

[SqlServerCondition(SqlServerCondition.SupportsFunctions2022)]
public override async Task TrimStart_with_char_array_argument_in_predicate(bool async)
{
// String.Trim with parameters. Issue #22927.
await AssertTranslationFailed(() => base.TrimStart_with_char_array_argument_in_predicate(async));
await base.TrimStart_with_char_array_argument_in_predicate(async);

AssertSql();
AssertSql(
"""
SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region]
FROM [Customers] AS [c]
WHERE LTRIM([c].[ContactTitle], N'Ow') = N'ner'
""");
}

public override async Task TrimEnd_without_arguments_in_predicate(bool async)
Expand All @@ -2684,20 +2694,30 @@ WHERE RTRIM([c].[ContactTitle]) = N'Owner'
""");
}

[SqlServerCondition(SqlServerCondition.SupportsFunctions2022)]
public override async Task TrimEnd_with_char_argument_in_predicate(bool async)
{
// String.Trim with parameters. Issue #22927.
await AssertTranslationFailed(() => base.TrimEnd_with_char_argument_in_predicate(async));
await base.TrimEnd_with_char_argument_in_predicate(async);

AssertSql();
AssertSql(
"""
SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region]
FROM [Customers] AS [c]
WHERE RTRIM([c].[ContactTitle], N'r') = N'Owne'
""");
}

[SqlServerCondition(SqlServerCondition.SupportsFunctions2022)]
public override async Task TrimEnd_with_char_array_argument_in_predicate(bool async)
{
// String.Trim with parameters. Issue #22927.
await AssertTranslationFailed(() => base.TrimEnd_with_char_array_argument_in_predicate(async));
await base.TrimEnd_with_char_array_argument_in_predicate(async);

AssertSql();
AssertSql(
"""
SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region]
FROM [Customers] AS [c]
WHERE RTRIM([c].[ContactTitle], N'er') = N'Own'
""");
}

public override async Task Trim_without_argument_in_predicate(bool async)
Expand Down

0 comments on commit 9e15699

Please sign in to comment.