Skip to content

Commit

Permalink
LINQ : Adds User Defined Function Translation Support (#2206)
Browse files Browse the repository at this point in the history
* turned on tests

* updated docs

* Matched suggestions from #1562

* Fixed unit tests to use instance.

* Added test for user defined UDF, fixed baseline expectations and updated API json file.

* Moved method into CosmosLinqExtensions

* Make the UDF name the extension property.

* Updated sample to match an extension method pattern.

* Updated API after documentation change

* Use static method instead of extension method.

* Fix contract file

* Removed unnecessary using

* Removed remaining changes to LinqExtensions

Co-authored-by: Brandon Chong <bchong95@users.noreply.github.com>
Co-authored-by: Dan Piessens <dan.piessens@tcreveal.com>
Co-authored-by: Jake Willey <jawilley@microsoft.com>
  • Loading branch information
4 people authored Feb 11, 2021
1 parent 368c64d commit dae2747
Show file tree
Hide file tree
Showing 7 changed files with 73 additions and 50 deletions.
2 changes: 1 addition & 1 deletion Microsoft.Azure.Cosmos/src/Linq/ConstantEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ private static bool CanBeEvaluated(Expression expression)
if (methodCallExpression != null)
{
Type type = methodCallExpression.Method.DeclaringType;
if (type == typeof(Enumerable) || type == typeof(Queryable) || type == typeof(UserDefinedFunctionProvider))
if (type == typeof(Enumerable) || type == typeof(Queryable) || type == typeof(CosmosLinq))
{
return false;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
//------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
//------------------------------------------------------------

namespace Microsoft.Azure.Cosmos.Linq
{
using System;
using System.Globalization;
using Microsoft.Azure.Cosmos.Scripts;

/// <summary>
/// Helper class to invoke User Defined Functions via Linq queries in the Azure Cosmos DB service.
/// This class provides methods for cosmos LINQ code.
/// </summary>
internal static class UserDefinedFunctionProvider
public static class CosmosLinq
{
/// <summary>
/// Helper method to invoke User Defined Functions via Linq queries in the Azure Cosmos DB service.
/// </summary>
/// <param name="udfName">the UserDefinedFunction name</param>
/// <param name="arguments">the arguments of the UserDefinedFunction</param>
/// <param name="udfName">The UserDefinedFunction name</param>
/// <param name="arguments">The arguments of the UserDefinedFunction</param>
/// <remarks>
/// This is a stub helper method for use within LINQ expressions. Cannot be called directly.
/// Refer to https://docs.microsoft.com/azure/cosmos-db/sql-query-linq-to-sql for more details about the LINQ provider.
Expand All @@ -25,19 +26,31 @@ internal static class UserDefinedFunctionProvider
/// <example>
/// <code language="c#">
/// <![CDATA[
/// await client.CreateUserDefinedFunctionAsync(collectionLink, new UserDefinedFunction { Id = "calculateTax", Body = @"function(amt) { return amt * 0.05; }" });
/// var queryable = client.CreateDocumentQuery<Book>(collectionLink).Select(b => UserDefinedFunctionProvider.Invoke("calculateTax", b.Price));
///
/// // Equivalent to SELECT * FROM books b WHERE udf.toLowerCase(b.title) = 'war and peace'"
/// await client.CreateUserDefinedFunctionAsync(collectionLink, new UserDefinedFunction { Id = "toLowerCase", Body = @"function(s) { return s.ToLowerCase(); }" });
/// queryable = client.CreateDocumentQuery<Book>(collectionLink).Where(b => UserDefinedFunctionProvider.Invoke("toLowerCase", b.Title) == "war and peace");
/// IQueryable<Book> queryable = client
/// .GetContainer("database", "container")
/// .GetItemLinqQueryable<Book>()
/// .Where(b => CosmosLinq.InvokeUserDefinedFunction("toLowerCase", b.Title) == "war and peace");
///
/// FeedIterator<Book> bookIterator = queryable.ToFeedIterator();
/// while (feedIterator.HasMoreResults)
/// {
/// FeedResponse<Book> responseMessage = await feedIterator.ReadNextAsync();
/// DoSomethingWithResponse(responseMessage);
/// }
/// ]]>
/// </code>
/// </example>
/// <seealso cref="UserDefinedFunctionProperties"/>
public static object Invoke(string udfName, params object[] arguments)
/// <returns>Placeholder for the udf result.</returns>
#pragma warning disable IDE0060 // Remove unused parameter
public static object InvokeUserDefinedFunction(string udfName, params object[] arguments)
#pragma warning restore IDE0060 // Remove unused parameter
{
throw new Exception(string.Format(CultureInfo.CurrentCulture, ClientResources.InvalidCallToUserDefinedFunctionProvider));
throw new NotSupportedException(
string.Format(
CultureInfo.CurrentCulture,
ClientResources.InvalidCallToUserDefinedFunctionProvider));
}
}
}
}
2 changes: 1 addition & 1 deletion Microsoft.Azure.Cosmos/src/Linq/CosmosLinqExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -724,4 +724,4 @@ private static MethodInfo GetMethodInfoOf<T1, T2>(Func<T1, T2> func)
return func.GetMethodInfo();
}
}
}
}
2 changes: 1 addition & 1 deletion Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ internal static SqlScalarExpression VisitNonSubqueryScalarExpression(Expression
private static SqlScalarExpression VisitMethodCallScalar(MethodCallExpression methodCallExpression, TranslationContext context)
{
// Check if it is a UDF method call
if (methodCallExpression.Method.Equals(typeof(UserDefinedFunctionProvider).GetMethod("Invoke")))
if (methodCallExpression.Method.Equals(typeof(CosmosLinq).GetMethod("InvokeUserDefinedFunction")))
{
string udfName = ((ConstantExpression)methodCallExpression.Arguments[0]).Value as string;
if (string.IsNullOrEmpty(udfName))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Result>
<Input>
<Description><![CDATA[No param]]></Description>
<Expression><![CDATA[query.Select(f => Invoke("NoParameterUDF", new [] {}))]]></Expression>
<Expression><![CDATA[query.Select(f => InvokeUserDefinedFunction("NoParameterUDF", new [] {}))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[
Expand All @@ -13,7 +13,7 @@ FROM root ]]></SqlQuery>
<Result>
<Input>
<Description><![CDATA[Single param]]></Description>
<Expression><![CDATA[query.Select(doc => Invoke("SingleParameterUDF", new [] {Convert(doc.NumericField, Object)}))]]></Expression>
<Expression><![CDATA[query.Select(doc => InvokeUserDefinedFunction("SingleParameterUDF", new [] {Convert(doc.NumericField, Object)}))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[
Expand All @@ -24,7 +24,7 @@ FROM root ]]></SqlQuery>
<Result>
<Input>
<Description><![CDATA[Single param w/ array]]></Description>
<Expression><![CDATA[query.Select(doc => Invoke("SingleParameterUDFWithArray", new [] {doc.ArrayField}))]]></Expression>
<Expression><![CDATA[query.Select(doc => InvokeUserDefinedFunction("SingleParameterUDFWithArray", new [] {doc.ArrayField}))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[
Expand All @@ -35,7 +35,7 @@ FROM root ]]></SqlQuery>
<Result>
<Input>
<Description><![CDATA[Multi param]]></Description>
<Expression><![CDATA[query.Select(doc => Invoke("MultiParamterUDF", new [] {Convert(doc.NumericField, Object), doc.StringField, doc.Point}))]]></Expression>
<Expression><![CDATA[query.Select(doc => InvokeUserDefinedFunction("MultiParamterUDF", new [] {Convert(doc.NumericField, Object), doc.StringField, doc.Point}))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[
Expand All @@ -46,7 +46,7 @@ FROM root ]]></SqlQuery>
<Result>
<Input>
<Description><![CDATA[Multi param w/ array]]></Description>
<Expression><![CDATA[query.Select(doc => Invoke("MultiParamterUDWithArrayF", new [] {doc.ArrayField, Convert(doc.NumericField, Object), doc.Point}))]]></Expression>
<Expression><![CDATA[query.Select(doc => InvokeUserDefinedFunction("MultiParamterUDWithArrayF", new [] {doc.ArrayField, Convert(doc.NumericField, Object), doc.Point}))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[
Expand All @@ -57,7 +57,7 @@ FROM root ]]></SqlQuery>
<Result>
<Input>
<Description><![CDATA[ArrayCount]]></Description>
<Expression><![CDATA[query.Where(doc => (Convert(Invoke("ArrayCount", new [] {doc.ArrayField}), Int32) > 2))]]></Expression>
<Expression><![CDATA[query.Where(doc => (Convert(InvokeUserDefinedFunction("ArrayCount", new [] {doc.ArrayField}), Int32) > 2))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[
Expand All @@ -69,7 +69,7 @@ WHERE (udf.ArrayCount(root["ArrayField"]) > 2) ]]></SqlQuery>
<Result>
<Input>
<Description><![CDATA[ArrayCount && SomeBooleanUDF]]></Description>
<Expression><![CDATA[query.Where(doc => ((Convert(Invoke("ArrayCount", new [] {doc.ArrayField}), Int32) > 2) AndAlso Convert(Invoke("SomeBooleanUDF", new [] {}), Boolean)))]]></Expression>
<Expression><![CDATA[query.Where(doc => ((Convert(InvokeUserDefinedFunction("ArrayCount", new [] {doc.ArrayField}), Int32) > 2) AndAlso Convert(InvokeUserDefinedFunction("SomeBooleanUDF", new [] {}), Boolean)))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[
Expand All @@ -81,7 +81,7 @@ WHERE ((udf.ArrayCount(root["ArrayField"]) > 2) AND udf.SomeBooleanUDF()) ]]></S
<Result>
<Input>
<Description><![CDATA[expression]]></Description>
<Expression><![CDATA[query.Where(doc => ((Convert(Invoke("SingleParameterUDF", new [] {Convert(doc.NumericField, Object)}), Int32) + 2) == 4))]]></Expression>
<Expression><![CDATA[query.Where(doc => ((Convert(InvokeUserDefinedFunction("SingleParameterUDF", new [] {Convert(doc.NumericField, Object)}), Int32) + 2) == 4))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[
Expand All @@ -93,7 +93,7 @@ WHERE ((udf.SingleParameterUDF(root["NumericField"]) + 2) = 4) ]]></SqlQuery>
<Result>
<Input>
<Description><![CDATA[Single constant param]]></Description>
<Expression><![CDATA[query.Select(doc => Invoke("SingleParameterUDF", new [] {Convert(1, Object)}))]]></Expression>
<Expression><![CDATA[query.Select(doc => InvokeUserDefinedFunction("SingleParameterUDF", new [] {Convert(1, Object)}))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[
Expand All @@ -104,7 +104,7 @@ FROM root ]]></SqlQuery>
<Result>
<Input>
<Description><![CDATA[Single constant int array param]]></Description>
<Expression><![CDATA[query.Select(doc => Invoke("SingleParameterUDFWithArray", new [] {new [] {1, 2, 3}}))]]></Expression>
<Expression><![CDATA[query.Select(doc => InvokeUserDefinedFunction("SingleParameterUDFWithArray", new [] {new [] {1, 2, 3}}))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[
Expand All @@ -115,7 +115,7 @@ FROM root ]]></SqlQuery>
<Result>
<Input>
<Description><![CDATA[Single constant string array param]]></Description>
<Expression><![CDATA[query.Select(doc => Invoke("SingleParameterUDFWithArray", new [] {"1", "2"}))]]></Expression>
<Expression><![CDATA[query.Select(doc => InvokeUserDefinedFunction("SingleParameterUDFWithArray", new [] {"1", "2"}))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[
Expand All @@ -126,7 +126,7 @@ FROM root ]]></SqlQuery>
<Result>
<Input>
<Description><![CDATA[Multi constant params]]></Description>
<Expression><![CDATA[query.Select(doc => Invoke("MultiParamterUDF", new [] {Convert(1, Object), "str", Convert(True, Object)}))]]></Expression>
<Expression><![CDATA[query.Select(doc => InvokeUserDefinedFunction("MultiParamterUDF", new [] {Convert(1, Object), "str", Convert(True, Object)}))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[
Expand All @@ -137,7 +137,7 @@ FROM root ]]></SqlQuery>
<Result>
<Input>
<Description><![CDATA[Multi constant array params]]></Description>
<Expression><![CDATA[query.Select(doc => Invoke("MultiParamterUDWithArrayF", new [] {new [] {1, 2, 3}, Convert(1, Object), "str"}))]]></Expression>
<Expression><![CDATA[query.Select(doc => InvokeUserDefinedFunction("MultiParamterUDWithArrayF", new [] {new [] {1, 2, 3}, Convert(1, Object), "str"}))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[
Expand All @@ -148,7 +148,7 @@ FROM root ]]></SqlQuery>
<Result>
<Input>
<Description><![CDATA[ArrayCount with constant param]]></Description>
<Expression><![CDATA[query.Where(doc => (Convert(Invoke("ArrayCount", new [] {new [] {1, 2, 3}}), Int32) > 2))]]></Expression>
<Expression><![CDATA[query.Where(doc => (Convert(InvokeUserDefinedFunction("ArrayCount", new [] {new [] {1, 2, 3}}), Int32) > 2))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[
Expand All @@ -160,7 +160,7 @@ WHERE (udf.ArrayCount([1, 2, 3]) > 2) ]]></SqlQuery>
<Result>
<Input>
<Description><![CDATA[different type parameters including objects]]></Description>
<Expression><![CDATA[query.Where(doc => Convert(Invoke("MultiParamterUDF2", new [] {doc.Point, "str", Convert(1, Object)}), Boolean))]]></Expression>
<Expression><![CDATA[query.Where(doc => Convert(InvokeUserDefinedFunction("MultiParamterUDF2", new [] {doc.Point, "str", Convert(1, Object)}), Boolean))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[
Expand All @@ -172,7 +172,7 @@ WHERE udf.MultiParamterUDF2(root["Point"], "str", 1) ]]></SqlQuery>
<Result>
<Input>
<Description><![CDATA[Null udf name]]></Description>
<Expression><![CDATA[query.Select(doc => Invoke(null, new [] {}))]]></Expression>
<Expression><![CDATA[query.Select(doc => InvokeUserDefinedFunction(null, new [] {}))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[]]></SqlQuery>
Expand All @@ -182,7 +182,7 @@ WHERE udf.MultiParamterUDF2(root["Point"], "str", 1) ]]></SqlQuery>
<Result>
<Input>
<Description><![CDATA[Empty udf name]]></Description>
<Expression><![CDATA[query.Select(doc => Invoke("", new [] {}))]]></Expression>
<Expression><![CDATA[query.Select(doc => InvokeUserDefinedFunction("", new [] {}))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[]]></SqlQuery>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -885,7 +885,6 @@ public void TestStringCompareTo()
}

[TestMethod]
[TestCategory("Ignore")]
public void TestUDFs()
{
// The UDFs invokation are not supported on the client side.
Expand All @@ -896,26 +895,26 @@ public void TestUDFs()

List<LinqTestInput> inputs = new List<LinqTestInput>
{
new LinqTestInput("No param", b => getQuery(b).Select(f => UserDefinedFunctionProvider.Invoke("NoParameterUDF"))),
new LinqTestInput("Single param", b => getQuery(b).Select(doc => UserDefinedFunctionProvider.Invoke("SingleParameterUDF", doc.NumericField))),
new LinqTestInput("Single param w/ array", b => getQuery(b).Select(doc => UserDefinedFunctionProvider.Invoke("SingleParameterUDFWithArray", doc.ArrayField))),
new LinqTestInput("Multi param", b => getQuery(b).Select(doc => UserDefinedFunctionProvider.Invoke("MultiParamterUDF", doc.NumericField, doc.StringField, doc.Point))),
new LinqTestInput("Multi param w/ array", b => getQuery(b).Select(doc => UserDefinedFunctionProvider.Invoke("MultiParamterUDWithArrayF", doc.ArrayField, doc.NumericField, doc.Point))),
new LinqTestInput("ArrayCount", b => getQuery(b).Where(doc => (int)UserDefinedFunctionProvider.Invoke("ArrayCount", doc.ArrayField) > 2)),
new LinqTestInput("ArrayCount && SomeBooleanUDF", b => getQuery(b).Where(doc => (int)UserDefinedFunctionProvider.Invoke("ArrayCount", doc.ArrayField) > 2 && (bool)UserDefinedFunctionProvider.Invoke("SomeBooleanUDF"))),
new LinqTestInput("expression", b => getQuery(b).Where(doc => (int)UserDefinedFunctionProvider.Invoke("SingleParameterUDF", doc.NumericField) + 2 == 4)),
new LinqTestInput("No param", b => getQuery(b).Select(f => CosmosLinq.InvokeUserDefinedFunction("NoParameterUDF"))),
new LinqTestInput("Single param", b => getQuery(b).Select(doc => CosmosLinq.InvokeUserDefinedFunction("SingleParameterUDF", doc.NumericField))),
new LinqTestInput("Single param w/ array", b => getQuery(b).Select(doc => CosmosLinq.InvokeUserDefinedFunction("SingleParameterUDFWithArray", doc.ArrayField))),
new LinqTestInput("Multi param", b => getQuery(b).Select(doc => CosmosLinq.InvokeUserDefinedFunction("MultiParamterUDF", doc.NumericField, doc.StringField, doc.Point))),
new LinqTestInput("Multi param w/ array", b => getQuery(b).Select(doc => CosmosLinq.InvokeUserDefinedFunction("MultiParamterUDWithArrayF", doc.ArrayField, doc.NumericField, doc.Point))),
new LinqTestInput("ArrayCount", b => getQuery(b).Where(doc => (int)CosmosLinq.InvokeUserDefinedFunction("ArrayCount", doc.ArrayField) > 2)),
new LinqTestInput("ArrayCount && SomeBooleanUDF", b => getQuery(b).Where(doc => (int)CosmosLinq.InvokeUserDefinedFunction("ArrayCount", doc.ArrayField) > 2 && (bool)CosmosLinq.InvokeUserDefinedFunction("SomeBooleanUDF"))),
new LinqTestInput("expression", b => getQuery(b).Where(doc => (int)CosmosLinq.InvokeUserDefinedFunction("SingleParameterUDF", doc.NumericField) + 2 == 4)),
// UDF with constant parameters
new LinqTestInput("Single constant param", b => getQuery(b).Select(doc => UserDefinedFunctionProvider.Invoke("SingleParameterUDF", 1))),
new LinqTestInput("Single constant int array param", b => getQuery(b).Select(doc => UserDefinedFunctionProvider.Invoke("SingleParameterUDFWithArray", new int[] { 1, 2, 3 }))),
new LinqTestInput("Single constant string array param", b => getQuery(b).Select(doc => UserDefinedFunctionProvider.Invoke("SingleParameterUDFWithArray", new string[] { "1", "2" }))),
new LinqTestInput("Multi constant params", b => getQuery(b).Select(doc => UserDefinedFunctionProvider.Invoke("MultiParamterUDF", 1, "str", true))),
new LinqTestInput("Multi constant array params", b => getQuery(b).Select(doc => UserDefinedFunctionProvider.Invoke("MultiParamterUDWithArrayF", new int[] { 1, 2, 3 }, 1, "str"))),
new LinqTestInput("ArrayCount with constant param", b => getQuery(b).Where(doc => (int)UserDefinedFunctionProvider.Invoke("ArrayCount", new int[] { 1, 2, 3 }) > 2)),
new LinqTestInput("Single constant param", b => getQuery(b).Select(doc => CosmosLinq.InvokeUserDefinedFunction("SingleParameterUDF", 1))),
new LinqTestInput("Single constant int array param", b => getQuery(b).Select(doc => CosmosLinq.InvokeUserDefinedFunction("SingleParameterUDFWithArray", new int[] { 1, 2, 3 }))),
new LinqTestInput("Single constant string array param", b => getQuery(b).Select(doc => CosmosLinq.InvokeUserDefinedFunction("SingleParameterUDFWithArray", new string[] { "1", "2" }))),
new LinqTestInput("Multi constant params", b => getQuery(b).Select(doc => CosmosLinq.InvokeUserDefinedFunction("MultiParamterUDF", 1, "str", true))),
new LinqTestInput("Multi constant array params", b => getQuery(b).Select(doc => CosmosLinq.InvokeUserDefinedFunction("MultiParamterUDWithArrayF", new int[] { 1, 2, 3 }, 1, "str"))),
new LinqTestInput("ArrayCount with constant param", b => getQuery(b).Where(doc => (int)CosmosLinq.InvokeUserDefinedFunction("ArrayCount", new int[] { 1, 2, 3 }) > 2)),
// regression (different type parameters including objects)
new LinqTestInput("different type parameters including objects", b => getQuery(b).Where(doc => (bool)UserDefinedFunctionProvider.Invoke("MultiParamterUDF2", doc.Point, "str", 1))),
new LinqTestInput("different type parameters including objects", b => getQuery(b).Where(doc => (bool)CosmosLinq.InvokeUserDefinedFunction("MultiParamterUDF2", doc.Point, "str", 1))),
// errors
new LinqTestInput("Null udf name", b => getQuery(b).Select(doc => UserDefinedFunctionProvider.Invoke(null))),
new LinqTestInput("Empty udf name", b => getQuery(b).Select(doc => UserDefinedFunctionProvider.Invoke("")))
new LinqTestInput("Null udf name", b => getQuery(b).Select(doc => CosmosLinq.InvokeUserDefinedFunction(null))),
new LinqTestInput("Empty udf name", b => getQuery(b).Select(doc => CosmosLinq.InvokeUserDefinedFunction("")))
};
this.ExecuteTestSuite(inputs);
}
Expand Down
Loading

0 comments on commit dae2747

Please sign in to comment.