Skip to content

Commit

Permalink
New EF.Functions.Collate
Browse files Browse the repository at this point in the history
Closes #8813
  • Loading branch information
roji committed Apr 23, 2020
1 parent 49c997d commit 61947ce
Show file tree
Hide file tree
Showing 14 changed files with 260 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Query;

// ReSharper disable once CheckNamespace
namespace Microsoft.EntityFrameworkCore
{
public static class RelationalDbFunctionsExtensions
{
/// <summary>
/// <para>
/// Explicitly specifies a collation to be used in a LINQ query. Can be used to generate fragments such as
/// <code>WHERE customer.name COLLATE 'de_DE' = 'John Doe'</code>.
/// </para>
/// <para>
/// The available collations and their names vary across databases, consult your database's documentation for more
/// information.
/// </para>
/// </summary>
/// <typeparam name="TProperty"> The type of the operand on which the collation is being specified. </typeparam>
/// <param name="_"> The DbFunctions instance. </param>
/// <param name="operand"> The operand to which to apply the collation. </param>
/// <param name="collation"> The name of the collation. </param>
public static TProperty Collate<TProperty>(
[NotNull] this DbFunctions _,
[NotNull] TProperty operand,
[NotNull] [NotParameterized] string collation)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Collate)));
}
}
36 changes: 36 additions & 0 deletions src/EFCore.Relational/Query/Internal/CollateTranslator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Generic;
using System.Reflection;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.Utilities;

namespace Microsoft.EntityFrameworkCore.Query.Internal
{
public class CollateTranslator : IMethodCallTranslator
{
private static readonly MethodInfo _methodInfo
= typeof(RelationalDbFunctionsExtensions).GetMethod(nameof(RelationalDbFunctionsExtensions.Collate));

private readonly ISqlExpressionFactory _sqlExpressionFactory;

public CollateTranslator([NotNull] ISqlExpressionFactory sqlExpressionFactory)
=> _sqlExpressionFactory = sqlExpressionFactory;

public virtual SqlExpression Translate(SqlExpression instance, MethodInfo method, IReadOnlyList<SqlExpression> arguments)
{
Check.NotNull(method, nameof(method));
Check.NotNull(arguments, nameof(arguments));

return method.IsGenericMethod
&& Equals(method.GetGenericMethodDefinition(), _methodInfo)
&& arguments[2] is SqlConstantExpression constantExpression
&& constantExpression.Value is string collation
? new CollateExpression(arguments[1], collation)
: null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,14 @@ protected override Expression VisitCase(CaseExpression caseExpression)
return caseExpression.Update(operand, whenClauses, elseResult);
}

protected override Expression VisitCollate(CollateExpression collateExpresion)
{
Check.NotNull(collateExpresion, nameof(collateExpresion));

return collateExpresion.Update(
VisitInternal<SqlExpression>(collateExpresion.Operand).ResultExpression);
}

protected override Expression VisitColumn(ColumnExpression columnExpression)
{
Check.NotNull(columnExpression, nameof(columnExpression));
Expand Down
13 changes: 13 additions & 0 deletions src/EFCore.Relational/Query/QuerySqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,19 @@ protected override Expression VisitLike(LikeExpression likeExpression)
return likeExpression;
}

protected override Expression VisitCollate(CollateExpression collateExpresion)
{
Check.NotNull(collateExpresion, nameof(collateExpresion));

Visit(collateExpresion.Operand);

_relationalCommandBuilder
.Append(" COLLATE ")
.Append(collateExpresion.Collation);

return collateExpresion;
}

protected override Expression VisitCase(CaseExpression caseExpression)
{
Check.NotNull(caseExpression, nameof(caseExpression));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public RelationalMethodCallTranslatorProvider([NotNull] RelationalMethodCallTran
{
new EqualsTranslator(sqlExpressionFactory),
new StringMethodTranslator(sqlExpressionFactory),
new CollateTranslator(sqlExpressionFactory),
new ContainsTranslator(sqlExpressionFactory),
new LikeTranslator(sqlExpressionFactory),
new EnumHasFlagTranslator(sqlExpressionFactory),
Expand Down
7 changes: 7 additions & 0 deletions src/EFCore.Relational/Query/SqlExpressionFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public virtual SqlExpression ApplyTypeMapping(SqlExpression sqlExpression, Relat
return sqlExpression switch
{
CaseExpression e => ApplyTypeMappingOnCase(e, typeMapping),
CollateExpression e => ApplyTypeMappingOnCollate(e, typeMapping),
LikeExpression e => ApplyTypeMappingOnLike(e),
SqlBinaryExpression e => ApplyTypeMappingOnSqlBinary(e, typeMapping),
SqlUnaryExpression e => ApplyTypeMappingOnSqlUnary(e, typeMapping),
Expand Down Expand Up @@ -102,6 +103,12 @@ private SqlExpression ApplyTypeMappingOnCase(
return caseExpression.Update(caseExpression.Operand, whenClauses, elseResult);
}

private SqlExpression ApplyTypeMappingOnCollate(
CollateExpression collateExpression, RelationalTypeMapping typeMapping)
=> new CollateExpression(
ApplyTypeMapping(collateExpression.Operand, typeMapping),
collateExpression.Collation);

private SqlExpression ApplyTypeMappingOnSqlUnary(
SqlUnaryExpression sqlUnaryExpression, RelationalTypeMapping typeMapping)
{
Expand Down
4 changes: 4 additions & 0 deletions src/EFCore.Relational/Query/SqlExpressionVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ protected override Expression VisitExtension(Expression extensionExpression)
case CaseExpression caseExpression:
return VisitCase(caseExpression);

case CollateExpression collateExpression:
return VisitCollate(collateExpression);

case ColumnExpression columnExpression:
return VisitColumn(columnExpression);

Expand Down Expand Up @@ -106,6 +109,7 @@ protected override Expression VisitExtension(Expression extensionExpression)
}

protected abstract Expression VisitCase([NotNull] CaseExpression caseExpression);
protected abstract Expression VisitCollate([NotNull] CollateExpression collateExpression);
protected abstract Expression VisitColumn([NotNull] ColumnExpression columnExpression);
protected abstract Expression VisitCrossApply([NotNull] CrossApplyExpression crossApplyExpression);
protected abstract Expression VisitCrossJoin([NotNull] CrossJoinExpression crossJoinExpression);
Expand Down
68 changes: 68 additions & 0 deletions src/EFCore.Relational/Query/SqlExpressions/CollateExpression.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Linq.Expressions;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Utilities;

namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions
{
public class CollateExpression : SqlExpression
{
public CollateExpression(
[NotNull] SqlExpression operand,
[NotNull] string collation)
: base(operand.Type, operand.TypeMapping)
{
Check.NotNull(operand, nameof(operand));
Check.NotEmpty(collation, nameof(collation));

Operand = operand;
Collation = collation;
}

public virtual SqlExpression Operand { get; }
public virtual string Collation { get; }

protected override Expression VisitChildren(ExpressionVisitor visitor)
{
Check.NotNull(visitor, nameof(visitor));

return Update((SqlExpression)visitor.Visit(Operand));
}

public virtual CollateExpression Update([NotNull] SqlExpression operand)
{
Check.NotNull(operand, nameof(operand));

return operand != Operand
? new CollateExpression(operand, Collation)
: this;
}

public override void Print(ExpressionPrinter expressionPrinter)
{
Check.NotNull(expressionPrinter, nameof(expressionPrinter));

expressionPrinter.Visit(Operand);
expressionPrinter
.Append(" COLLATE ")
.Append(Collation);
}

public override bool Equals(object obj)
=> obj != null
&& (ReferenceEquals(this, obj)
|| obj is CollateExpression collateExpression
&& Equals(collateExpression));

private bool Equals(CollateExpression collateExpression)
=> base.Equals(collateExpression)
&& Operand.Equals(collateExpression.Operand)
&& Collation.Equals(collateExpression.Collation, StringComparison.Ordinal);

public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), Operand, Collation);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,18 @@ protected override Expression VisitCase(CaseExpression caseExpression)
return ApplyConversion(caseExpression.Update(operand, whenClauses, elseResult), condition: false);
}

protected override Expression VisitCollate(CollateExpression collateExpression)
{
Check.NotNull(collateExpression, nameof(collateExpression));

var parentSearchCondition = _isSearchCondition;
_isSearchCondition = false;
var operand = (SqlExpression)Visit(collateExpression.Operand);
_isSearchCondition = parentSearchCondition;

return ApplyConversion(collateExpression.Update(operand), condition: false);
}

protected override Expression VisitColumn(ColumnExpression columnExpression)
{
Check.NotNull(columnExpression, nameof(columnExpression));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

namespace Microsoft.EntityFrameworkCore
{
public class MigrationsTestBase<TFixture> : IClassFixture<TFixture>
public abstract class MigrationsTestBase<TFixture> : IClassFixture<TFixture>
where TFixture : MigrationsTestBase<TFixture>.MigrationsFixtureBase, new()
{
private readonly ISqlGenerationHelper _sqlGenerationHelper;
Expand Down Expand Up @@ -1396,9 +1396,7 @@ private class Person
public int Age { get; set; }
}

protected virtual string NonDefaultCollation
=> throw new NotSupportedException(
$"Providers must override the '{nameof(NonDefaultCollation)}' property with a valid, non-default collation name for your database.");
protected abstract string NonDefaultCollation { get; }

protected virtual DbContext CreateContext() => Fixture.CreateContext();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.TestModels.Northwind;
using Microsoft.EntityFrameworkCore.TestUtilities;
using Xunit;

namespace Microsoft.EntityFrameworkCore.Query
{
public abstract class RelationalNorthwindDbFunctionsQueryTestBase<TFixture> : NorthwindDbFunctionsQueryTestBase<TFixture>
where TFixture : NorthwindQueryRelationalFixture<NoopModelCustomizer>, new()
{
public RelationalNorthwindDbFunctionsQueryTestBase(TFixture fixture)
: base(fixture)
{
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Collate_case_insensitive(bool async)
=> AssertCount(
async,
ss => ss.Set<Customer>(),
ss => ss.Set<Customer>(),
c => EF.Functions.Collate(c.ContactName, CaseInsensitiveCollation) == "maria anders",
c => c.ContactName.Equals("maria anders", StringComparison.OrdinalIgnoreCase));

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Collate_case_sensitive(bool async)
=> AssertCount(
async,
ss => ss.Set<Customer>(),
ss => ss.Set<Customer>(),
c => EF.Functions.Collate(c.ContactName, CaseSensitiveCollation) == "maria anders",
c => c.ContactName.Equals("maria anders", StringComparison.Ordinal));

protected abstract string CaseInsensitiveCollation { get; }
protected abstract string CaseSensitiveCollation { get; }
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.TestModels.Northwind;
using Microsoft.EntityFrameworkCore.TestUtilities;
Expand All @@ -20,38 +21,32 @@ protected NorthwindDbFunctionsQueryTestBase(TFixture fixture)
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Like_literal(bool async)
{
return AssertCount(
=> AssertCount(
async,
ss => ss.Set<Customer>(),
ss => ss.Set<Customer>(),
c => EF.Functions.Like(c.ContactName, "%M%"),
c => c.ContactName.Contains("M") || c.ContactName.Contains("m"));
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Like_identity(bool async)
{
return AssertCount(
=> AssertCount(
async,
ss => ss.Set<Customer>(),
ss => ss.Set<Customer>(),
c => EF.Functions.Like(c.ContactName, c.ContactName),
c => true);
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Like_literal_with_escape(bool async)
{
return AssertCount(
=> AssertCount(
async,
ss => ss.Set<Customer>(),
ss => ss.Set<Customer>(),
c => EF.Functions.Like(c.ContactName, "!%", "!"),
c => c.ContactName.Contains("%"));
}

protected NorthwindContext CreateContext() => Fixture.CreateContext();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
// ReSharper disable InconsistentNaming
namespace Microsoft.EntityFrameworkCore.Query
{
public class NorthwindDbFunctionsQuerySqlServerTest : NorthwindDbFunctionsQueryTestBase<NorthwindQuerySqlServerFixture<NoopModelCustomizer>>
public class NorthwindDbFunctionsQuerySqlServerTest : RelationalNorthwindDbFunctionsQueryTestBase<NorthwindQuerySqlServerFixture<NoopModelCustomizer>>
{
public NorthwindDbFunctionsQuerySqlServerTest(
NorthwindQuerySqlServerFixture<NoopModelCustomizer> fixture, ITestOutputHelper testOutputHelper)
Expand Down Expand Up @@ -54,6 +54,29 @@ FROM [Customers] AS [c]
WHERE [c].[ContactName] LIKE N'!%' ESCAPE N'!'");
}

public override async Task Collate_case_insensitive(bool async)
{
await base.Collate_case_insensitive(async);

AssertSql(
@"SELECT COUNT(*)
FROM [Customers] AS [c]
WHERE [c].[ContactName] COLLATE Latin1_General_CI_AI = N'maria anders'");
}

public override async Task Collate_case_sensitive(bool async)
{
await base.Collate_case_sensitive(async);

AssertSql(
@"SELECT COUNT(*)
FROM [Customers] AS [c]
WHERE [c].[ContactName] COLLATE Latin1_General_CS_AS = N'maria anders'");
}

protected override string CaseInsensitiveCollation => "Latin1_General_CI_AI";
protected override string CaseSensitiveCollation => "Latin1_General_CS_AS";

[ConditionalFact]
[SqlServerCondition(SqlServerCondition.SupportsFullTextSearch)]
public async Task FreeText_literal()
Expand Down
Loading

0 comments on commit 61947ce

Please sign in to comment.