From d34b02c1066234b3adf4810ab02ce18d1dcc5f9a Mon Sep 17 00:00:00 2001 From: Jacob Viau Date: Fri, 10 Jan 2020 23:55:55 -0800 Subject: [PATCH] Cosmos: translate string Contains, StartsWith, and EndsWith (#19563) Summary of changes - Adds initial StringMethodTranslator for Cosmos provider. - Translates Contains, StartsWith, and EndsWith Addresses part of #16143 --- .../CosmosMethodCallTranslatorProvider.cs | 2 +- .../Query/Internal/SqlExpressionFactory.cs | 3 + .../Query/Internal/StringMethodTranslator.cs | 81 +++++++++++++++++++ .../NorthwindFunctionsQueryCosmosTest.cs | 37 +++------ 4 files changed, 97 insertions(+), 26 deletions(-) create mode 100644 src/EFCore.Cosmos/Query/Internal/StringMethodTranslator.cs diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosMethodCallTranslatorProvider.cs b/src/EFCore.Cosmos/Query/Internal/CosmosMethodCallTranslatorProvider.cs index e6007b2344f..37e983cbd81 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosMethodCallTranslatorProvider.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosMethodCallTranslatorProvider.cs @@ -37,7 +37,7 @@ public CosmosMethodCallTranslatorProvider( new IMethodCallTranslator[] { new EqualsTranslator(sqlExpressionFactory), - //new StringMethodTranslator(sqlExpressionFactory), + new StringMethodTranslator(sqlExpressionFactory), new ContainsTranslator(sqlExpressionFactory) //new LikeTranslator(sqlExpressionFactory), //new EnumHasFlagTranslator(sqlExpressionFactory), diff --git a/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs b/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs index 5956c5c850d..01281c5a696 100644 --- a/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs +++ b/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs @@ -85,6 +85,9 @@ public virtual SqlExpression ApplyTypeMapping(SqlExpression sqlExpression, CoreT case SqlParameterExpression sqlParameterExpression: return sqlParameterExpression.ApplyTypeMapping(typeMapping); + case SqlFunctionExpression sqlFunctionExpression: + return sqlFunctionExpression.ApplyTypeMapping(typeMapping); + default: return sqlExpression; } diff --git a/src/EFCore.Cosmos/Query/Internal/StringMethodTranslator.cs b/src/EFCore.Cosmos/Query/Internal/StringMethodTranslator.cs new file mode 100644 index 00000000000..55322524dbb --- /dev/null +++ b/src/EFCore.Cosmos/Query/Internal/StringMethodTranslator.cs @@ -0,0 +1,81 @@ +// 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.Collections.Generic; +using System.Reflection; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal +{ + /// + /// 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. + /// + public class StringMethodTranslator : IMethodCallTranslator + { + private static readonly MethodInfo _containsMethodInfo + = typeof(string).GetRuntimeMethod(nameof(string.Contains), new[] { typeof(string) }); + + private static readonly MethodInfo _startsWithMethodInfo + = typeof(string).GetRuntimeMethod(nameof(string.StartsWith), new[] { typeof(string) }); + + private static readonly MethodInfo _endsWithMethodInfo + = typeof(string).GetRuntimeMethod(nameof(string.EndsWith), new[] { typeof(string) }); + + private readonly ISqlExpressionFactory _sqlExpressionFactory; + + /// + /// 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. + /// + public StringMethodTranslator([NotNull] ISqlExpressionFactory sqlExpressionFactory) + { + _sqlExpressionFactory = sqlExpressionFactory; + } + + /// + /// 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. + /// + public virtual SqlExpression Translate(SqlExpression instance, MethodInfo method, IReadOnlyList arguments) + { + Check.NotNull(method, nameof(method)); + Check.NotNull(arguments, nameof(arguments)); + + if (_containsMethodInfo.Equals(method)) + { + return TranslateSystemFunction("CONTAINS", instance, arguments[0], typeof(bool)); + } + + if (_startsWithMethodInfo.Equals(method)) + { + return TranslateSystemFunction("STARTSWITH", instance, arguments[0], typeof(bool)); + } + + if (_endsWithMethodInfo.Equals(method)) + { + return TranslateSystemFunction("ENDSWITH", instance, arguments[0], typeof(bool)); + } + + return null; + } + + private SqlExpression TranslateSystemFunction(string function, SqlExpression instance, SqlExpression pattern, Type returnType) + { + Check.NotNull(instance, nameof(instance)); + return _sqlExpressionFactory.ApplyDefaultTypeMapping( + _sqlExpressionFactory.Function( + function, + new[] { instance, pattern }, + returnType)); + } + } +} diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs index 3fe74c7f6cc..f7269f0bafc 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs @@ -19,7 +19,6 @@ public NorthwindFunctionsQueryCosmosTest( //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } - [ConditionalTheory(Skip = "Issue #17246")] public override async Task String_StartsWith_Literal(bool async) { await base.String_StartsWith_Literal(async); @@ -27,10 +26,9 @@ public override async Task String_StartsWith_Literal(bool async) AssertSql( @"SELECT c FROM root c -WHERE (c[""Discriminator""] = ""Customer"")"); +WHERE ((c[""Discriminator""] = ""Customer"") AND ((c[""ContactName""] != null) AND ((""M"" != null) AND STARTSWITH(c[""ContactName""], ""M""))))"); } - [ConditionalTheory(Skip = "Issue #17246")] public override async Task String_StartsWith_Identity(bool async) { await base.String_StartsWith_Identity(async); @@ -38,10 +36,9 @@ public override async Task String_StartsWith_Identity(bool async) AssertSql( @"SELECT c FROM root c -WHERE (c[""Discriminator""] = ""Customer"")"); +WHERE ((c[""Discriminator""] = ""Customer"") AND ((c[""ContactName""] = """") OR ((c[""ContactName""] != null) AND ((c[""ContactName""] != null) AND STARTSWITH(c[""ContactName""], c[""ContactName""])))))"); } - [ConditionalTheory(Skip = "Issue #17246")] public override async Task String_StartsWith_Column(bool async) { await base.String_StartsWith_Column(async); @@ -49,10 +46,9 @@ public override async Task String_StartsWith_Column(bool async) AssertSql( @"SELECT c FROM root c -WHERE (c[""Discriminator""] = ""Customer"")"); +WHERE ((c[""Discriminator""] = ""Customer"") AND ((c[""ContactName""] = """") OR ((c[""ContactName""] != null) AND ((c[""ContactName""] != null) AND STARTSWITH(c[""ContactName""], c[""ContactName""])))))"); } - [ConditionalTheory(Skip = "Issue #17246")] public override async Task String_StartsWith_MethodCall(bool async) { await base.String_StartsWith_MethodCall(async); @@ -60,10 +56,9 @@ public override async Task String_StartsWith_MethodCall(bool async) AssertSql( @"SELECT c FROM root c -WHERE (c[""Discriminator""] = ""Customer"")"); +WHERE ((c[""Discriminator""] = ""Customer"") AND ((c[""ContactName""] != null) AND ((""M"" != null) AND STARTSWITH(c[""ContactName""], ""M""))))"); } - [ConditionalTheory(Skip = "Issue #17246")] public override async Task String_EndsWith_Literal(bool async) { await base.String_EndsWith_Literal(async); @@ -71,10 +66,9 @@ public override async Task String_EndsWith_Literal(bool async) AssertSql( @"SELECT c FROM root c -WHERE (c[""Discriminator""] = ""Customer"")"); +WHERE ((c[""Discriminator""] = ""Customer"") AND ((c[""ContactName""] != null) AND ((""b"" != null) AND ENDSWITH(c[""ContactName""], ""b""))))"); } - [ConditionalTheory(Skip = "Issue #17246")] public override async Task String_EndsWith_Identity(bool async) { await base.String_EndsWith_Identity(async); @@ -82,10 +76,9 @@ public override async Task String_EndsWith_Identity(bool async) AssertSql( @"SELECT c FROM root c -WHERE (c[""Discriminator""] = ""Customer"")"); +WHERE ((c[""Discriminator""] = ""Customer"") AND ((c[""ContactName""] = """") OR ((c[""ContactName""] != null) AND ((c[""ContactName""] != null) AND ENDSWITH(c[""ContactName""], c[""ContactName""])))))"); } - [ConditionalTheory(Skip = "Issue #17246")] public override async Task String_EndsWith_Column(bool async) { await base.String_EndsWith_Column(async); @@ -93,10 +86,9 @@ public override async Task String_EndsWith_Column(bool async) AssertSql( @"SELECT c FROM root c -WHERE (c[""Discriminator""] = ""Customer"")"); +WHERE ((c[""Discriminator""] = ""Customer"") AND ((c[""ContactName""] = """") OR ((c[""ContactName""] != null) AND ((c[""ContactName""] != null) AND ENDSWITH(c[""ContactName""], c[""ContactName""])))))"); } - [ConditionalTheory(Skip = "Issue #17246")] public override async Task String_EndsWith_MethodCall(bool async) { await base.String_EndsWith_MethodCall(async); @@ -104,10 +96,9 @@ public override async Task String_EndsWith_MethodCall(bool async) AssertSql( @"SELECT c FROM root c -WHERE (c[""Discriminator""] = ""Customer"")"); +WHERE ((c[""Discriminator""] = ""Customer"") AND ((c[""ContactName""] != null) AND ((""m"" != null) AND ENDSWITH(c[""ContactName""], ""m""))))"); } - [ConditionalTheory(Skip = "Issue #17246")] public override async Task String_Contains_Literal(bool async) { await base.String_Contains_Literal(async); @@ -115,21 +106,18 @@ public override async Task String_Contains_Literal(bool async) AssertSql( @"SELECT c FROM root c -WHERE (c[""Discriminator""] = ""Customer"")"); +WHERE ((c[""Discriminator""] = ""Customer"") AND CONTAINS(c[""ContactName""], ""M""))"); } - [ConditionalTheory(Skip = "Issue #17246")] public override async Task String_Contains_Identity(bool async) { await base.String_Contains_Identity(async); - AssertSql( @"SELECT c FROM root c -WHERE (c[""Discriminator""] = ""Customer"")"); +WHERE ((c[""Discriminator""] = ""Customer"") AND CONTAINS(c[""ContactName""], c[""ContactName""]))"); } - [ConditionalTheory(Skip = "Issue #17246")] public override async Task String_Contains_Column(bool async) { await base.String_Contains_Column(async); @@ -137,10 +125,9 @@ public override async Task String_Contains_Column(bool async) AssertSql( @"SELECT c FROM root c -WHERE (c[""Discriminator""] = ""Customer"")"); +WHERE ((c[""Discriminator""] = ""Customer"") AND CONTAINS(c[""ContactName""], c[""ContactName""]))"); } - [ConditionalTheory(Skip = "Issue #17246")] public override async Task String_Contains_MethodCall(bool async) { await base.String_Contains_MethodCall(async); @@ -148,7 +135,7 @@ public override async Task String_Contains_MethodCall(bool async) AssertSql( @"SELECT c FROM root c -WHERE (c[""Discriminator""] = ""Customer"")"); +WHERE ((c[""Discriminator""] = ""Customer"") AND CONTAINS(c[""ContactName""], ""M""))"); } [ConditionalTheory(Skip = "Issue #17246")]