Skip to content

Commit 7711480

Browse files
leminh98Minh Le
andauthored
Query: Adds LINQ extension method for ORDER BY RANK, FullTextScore and RRF (#4993)
# Pull Request Template ## Description This PR adds extension methods for ORDER BY RANK, FullTextScore, and RRF to the LINQ extension method support. ## Type of change Please delete options that are not relevant. - [] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [] This change requires a documentation update ## Closing issues To automatically close an issue: closes #IssueNumber --------- Co-authored-by: Minh Le <leminh@microsoft.com>
1 parent 0b90eee commit 7711480

13 files changed

Lines changed: 4038 additions & 3175 deletions
Lines changed: 123 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,121 +1,124 @@
1-
//------------------------------------------------------------
2-
// Copyright (c) Microsoft Corporation. All rights reserved.
3-
//------------------------------------------------------------
4-
5-
namespace Microsoft.Azure.Cosmos.Linq
6-
{
7-
using System;
8-
using System.Collections.Generic;
9-
using System.Globalization;
10-
using System.Linq.Expressions;
11-
using Microsoft.Azure.Cosmos;
12-
using Microsoft.Azure.Cosmos.Spatial;
13-
using Microsoft.Azure.Cosmos.SqlObjects;
14-
using Microsoft.Azure.Documents;
15-
16-
internal abstract class BuiltinFunctionVisitor
17-
{
18-
public SqlScalarExpression Visit(MethodCallExpression methodCallExpression, TranslationContext context)
19-
{
20-
SqlScalarExpression result = this.VisitExplicit(methodCallExpression, context);
21-
if (result != null)
22-
{
23-
return result;
24-
}
25-
26-
result = this.VisitImplicit(methodCallExpression, context);
27-
if (result != null)
28-
{
29-
return result;
30-
}
31-
32-
throw new DocumentQueryException(string.Format(CultureInfo.CurrentCulture, ClientResources.MethodNotSupported, methodCallExpression.Method.Name));
33-
}
34-
35-
public static SqlScalarExpression VisitBuiltinFunctionCall(MethodCallExpression methodCallExpression, TranslationContext context)
36-
{
37-
Type declaringType;
38-
39-
// Method could be an extension method
40-
if (methodCallExpression.Method.IsStatic && methodCallExpression.Method.IsExtensionMethod())
41-
{
42-
if (methodCallExpression.Arguments.Count < 1)
43-
{
44-
// Extension methods should has at least 1 argument, this should never happen
45-
// Throwing ArgumentException instead of assert
46-
throw new ArgumentException();
47-
}
48-
49-
declaringType = methodCallExpression.Arguments[0].Type;
50-
51-
if (methodCallExpression.Method.DeclaringType.GeUnderlyingSystemType() == typeof(CosmosLinqExtensions))
52-
{
53-
// CosmosLinq Extensions can be RegexMatch, DocumentId or Type check functions (IsString, IsBool, etc.)
54-
if ((methodCallExpression.Method.Name == nameof(CosmosLinqExtensions.RegexMatch)) ||
55-
(methodCallExpression.Method.Name == nameof(CosmosLinqExtensions.FullTextContains)) ||
56-
(methodCallExpression.Method.Name == nameof(CosmosLinqExtensions.FullTextContainsAll)) ||
57-
(methodCallExpression.Method.Name == nameof(CosmosLinqExtensions.FullTextContainsAny)))
58-
{
59-
return StringBuiltinFunctions.Visit(methodCallExpression, context);
1+
//------------------------------------------------------------
2+
// Copyright (c) Microsoft Corporation. All rights reserved.
3+
//------------------------------------------------------------
4+
5+
namespace Microsoft.Azure.Cosmos.Linq
6+
{
7+
using System;
8+
using System.Collections.Generic;
9+
using System.Globalization;
10+
using System.Linq.Expressions;
11+
using Microsoft.Azure.Cosmos;
12+
using Microsoft.Azure.Cosmos.Spatial;
13+
using Microsoft.Azure.Cosmos.SqlObjects;
14+
using Microsoft.Azure.Documents;
15+
16+
internal abstract class BuiltinFunctionVisitor
17+
{
18+
public SqlScalarExpression Visit(MethodCallExpression methodCallExpression, TranslationContext context)
19+
{
20+
SqlScalarExpression result = this.VisitExplicit(methodCallExpression, context);
21+
if (result != null)
22+
{
23+
return result;
24+
}
25+
26+
result = this.VisitImplicit(methodCallExpression, context);
27+
if (result != null)
28+
{
29+
return result;
30+
}
31+
32+
throw new DocumentQueryException(string.Format(CultureInfo.CurrentCulture, ClientResources.MethodNotSupported, methodCallExpression.Method.Name));
33+
}
34+
35+
public static SqlScalarExpression VisitBuiltinFunctionCall(MethodCallExpression methodCallExpression, TranslationContext context)
36+
{
37+
Type declaringType;
38+
bool isExtensionMethod = methodCallExpression.Method.IsExtensionMethod();
39+
// Method could be an extension method
40+
// RRF doesn't have "this" qualifier, so it's not considered an extension method by the compiler, and so needed to be checked separately
41+
if (methodCallExpression.Method.IsStatic &&
42+
(methodCallExpression.Method.IsExtensionMethod()
43+
|| methodCallExpression.Method.Name.Equals(nameof(CosmosLinqExtensions.RRF))))
44+
{
45+
if (methodCallExpression.Arguments.Count < 1)
46+
{
47+
// Extension methods should has at least 1 argument, this should never happen
48+
// Throwing ArgumentException instead of assert
49+
throw new ArgumentException();
50+
}
51+
52+
declaringType = methodCallExpression.Arguments[0].Type;
53+
54+
if (methodCallExpression.Method.DeclaringType.GeUnderlyingSystemType() == typeof(CosmosLinqExtensions))
55+
{
56+
// CosmosLinq Extensions can be RegexMatch, DocumentId or Type check functions (IsString, IsBool, etc.)
57+
switch (methodCallExpression.Method.Name)
58+
{
59+
case nameof(CosmosLinqExtensions.RegexMatch):
60+
case nameof(CosmosLinqExtensions.FullTextContains):
61+
case nameof(CosmosLinqExtensions.FullTextContainsAll):
62+
case nameof(CosmosLinqExtensions.FullTextContainsAny):
63+
return StringBuiltinFunctions.Visit(methodCallExpression, context);
64+
case nameof(CosmosLinqExtensions.DocumentId):
65+
case nameof(CosmosLinqExtensions.RRF):
66+
case nameof(CosmosLinqExtensions.FullTextScore):
67+
return OtherBuiltinSystemFunctions.Visit(methodCallExpression, context);
68+
default:
69+
return TypeCheckFunctions.Visit(methodCallExpression, context);
6070
}
61-
62-
if (methodCallExpression.Method.Name == nameof(CosmosLinqExtensions.DocumentId))
63-
{
64-
return OtherBuiltinSystemFunctions.Visit(methodCallExpression, context);
65-
}
66-
67-
return TypeCheckFunctions.Visit(methodCallExpression, context);
68-
}
69-
}
70-
else
71-
{
72-
declaringType = methodCallExpression.Method.DeclaringType;
73-
}
74-
75-
// Check order matters, some extension methods work for both strings and arrays
76-
77-
// Math functions
78-
if (declaringType == typeof(Math))
79-
{
80-
return MathBuiltinFunctions.Visit(methodCallExpression, context);
81-
}
82-
83-
// ToString with String and Guid only becomes passthrough
84-
if (methodCallExpression.Method.Name == "ToString" &&
85-
methodCallExpression.Arguments.Count == 0 &&
86-
methodCallExpression.Object != null &&
87-
((methodCallExpression.Object.Type == typeof(string)) ||
88-
(methodCallExpression.Object.Type == typeof(Guid))))
89-
{
90-
return ExpressionToSql.VisitNonSubqueryScalarExpression(methodCallExpression.Object, context);
91-
}
92-
93-
// String functions or ToString with Objects that are not strings and guids
94-
if ((declaringType == typeof(string)) ||
95-
(methodCallExpression.Method.Name == "ToString" &&
96-
methodCallExpression.Arguments.Count == 0 &&
97-
methodCallExpression.Object != null))
98-
{
99-
return StringBuiltinFunctions.Visit(methodCallExpression, context);
100-
}
101-
102-
// Array functions
103-
if (declaringType.IsEnumerable())
104-
{
105-
return ArrayBuiltinFunctions.Visit(methodCallExpression, context);
106-
}
107-
108-
// Spatial functions
109-
if (typeof(Geometry).IsAssignableFrom(declaringType))
110-
{
111-
return SpatialBuiltinFunctions.Visit(methodCallExpression, context);
112-
}
113-
114-
throw new DocumentQueryException(string.Format(CultureInfo.CurrentCulture, ClientResources.MethodNotSupported, methodCallExpression.Method.Name));
115-
}
116-
117-
protected abstract SqlScalarExpression VisitExplicit(MethodCallExpression methodCallExpression, TranslationContext context);
118-
119-
protected abstract SqlScalarExpression VisitImplicit(MethodCallExpression methodCallExpression, TranslationContext context);
120-
}
121-
}
71+
}
72+
}
73+
else
74+
{
75+
declaringType = methodCallExpression.Method.DeclaringType;
76+
}
77+
78+
// Check order matters, some extension methods work for both strings and arrays
79+
80+
// Math functions
81+
if (declaringType == typeof(Math))
82+
{
83+
return MathBuiltinFunctions.Visit(methodCallExpression, context);
84+
}
85+
86+
// ToString with String and Guid only becomes passthrough
87+
if (methodCallExpression.Method.Name == "ToString" &&
88+
methodCallExpression.Arguments.Count == 0 &&
89+
methodCallExpression.Object != null &&
90+
((methodCallExpression.Object.Type == typeof(string)) ||
91+
(methodCallExpression.Object.Type == typeof(Guid))))
92+
{
93+
return ExpressionToSql.VisitNonSubqueryScalarExpression(methodCallExpression.Object, context);
94+
}
95+
96+
// String functions or ToString with Objects that are not strings and guids
97+
if ((declaringType == typeof(string)) ||
98+
(methodCallExpression.Method.Name == "ToString" &&
99+
methodCallExpression.Arguments.Count == 0 &&
100+
methodCallExpression.Object != null))
101+
{
102+
return StringBuiltinFunctions.Visit(methodCallExpression, context);
103+
}
104+
105+
// Array functions
106+
if (declaringType.IsEnumerable())
107+
{
108+
return ArrayBuiltinFunctions.Visit(methodCallExpression, context);
109+
}
110+
111+
// Spatial functions
112+
if (typeof(Geometry).IsAssignableFrom(declaringType))
113+
{
114+
return SpatialBuiltinFunctions.Visit(methodCallExpression, context);
115+
}
116+
117+
throw new DocumentQueryException(string.Format(CultureInfo.CurrentCulture, ClientResources.MethodNotSupported, methodCallExpression.Method.Name));
118+
}
119+
120+
protected abstract SqlScalarExpression VisitExplicit(MethodCallExpression methodCallExpression, TranslationContext context);
121+
122+
protected abstract SqlScalarExpression VisitImplicit(MethodCallExpression methodCallExpression, TranslationContext context);
123+
}
124+
}

Microsoft.Azure.Cosmos/src/Linq/BuiltinFunctions/OtherBuiltinSystemFunctions.cs

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,88 @@ namespace Microsoft.Azure.Cosmos.Linq
66
{
77
using System;
88
using System.Collections.Generic;
9+
using System.Collections.Immutable;
10+
using System.Collections.ObjectModel;
911
using System.Globalization;
1012
using System.Linq.Expressions;
1113
using Microsoft.Azure.Cosmos.SqlObjects;
1214

1315
internal static class OtherBuiltinSystemFunctions
1416
{
17+
private class RRFVisit : SqlBuiltinFunctionVisitor
18+
{
19+
public RRFVisit()
20+
: base("RRF",
21+
true,
22+
new List<Type[]>()
23+
{
24+
new Type[]{typeof(Func<object, object>[])}
25+
})
26+
{
27+
}
28+
29+
protected override SqlScalarExpression VisitImplicit(MethodCallExpression methodCallExpression, TranslationContext context)
30+
{
31+
if (methodCallExpression.Arguments.Count == 1
32+
&& methodCallExpression.Arguments[0] is NewArrayExpression argumentsExpressions)
33+
{
34+
// For RRF, We don't need to care about the first argument, it is the object itself and have no relevance to the computation
35+
ReadOnlyCollection<Expression> functionListExpression = argumentsExpressions.Expressions;
36+
List<SqlScalarExpression> arguments = new List<SqlScalarExpression>();
37+
foreach (Expression argument in functionListExpression)
38+
{
39+
arguments.Add(ExpressionToSql.VisitScalarExpression(argument, context));
40+
}
41+
42+
return SqlFunctionCallScalarExpression.CreateBuiltin(SqlFunctionCallScalarExpression.Names.RRF, arguments.ToImmutableArray());
43+
}
44+
45+
return null;
46+
}
47+
48+
protected override SqlScalarExpression VisitExplicit(MethodCallExpression methodCallExpression, TranslationContext context)
49+
{
50+
return null;
51+
}
52+
}
53+
54+
private class FullTextScoreVisit : SqlBuiltinFunctionVisitor
55+
{
56+
public FullTextScoreVisit()
57+
: base("FullTextScore",
58+
true,
59+
new List<Type[]>()
60+
{
61+
new Type[]{typeof(object), typeof(string[])}
62+
})
63+
{
64+
}
65+
66+
protected override SqlScalarExpression VisitImplicit(MethodCallExpression methodCallExpression, TranslationContext context)
67+
{
68+
if (methodCallExpression.Arguments.Count == 2
69+
&& methodCallExpression.Arguments[1] is ConstantExpression stringListArgumentExpression
70+
&& ExpressionToSql.VisitConstant(stringListArgumentExpression, context) is SqlArrayCreateScalarExpression arrayScalarExpressions)
71+
{
72+
List<SqlScalarExpression> arguments = new List<SqlScalarExpression>
73+
{
74+
ExpressionToSql.VisitNonSubqueryScalarExpression(methodCallExpression.Arguments[0], context)
75+
};
76+
77+
arguments.AddRange(arrayScalarExpressions.Items);
78+
79+
return SqlFunctionCallScalarExpression.CreateBuiltin(SqlFunctionCallScalarExpression.Names.FullTextScore, arguments.ToImmutableArray());
80+
}
81+
82+
return null;
83+
}
84+
85+
protected override SqlScalarExpression VisitExplicit(MethodCallExpression methodCallExpression, TranslationContext context)
86+
{
87+
return null;
88+
}
89+
}
90+
1591
private static Dictionary<string, BuiltinFunctionVisitor> FunctionsDefinitions { get; set; }
1692

1793
static OtherBuiltinSystemFunctions()
@@ -24,7 +100,9 @@ static OtherBuiltinSystemFunctions()
24100
argumentLists: new List<Type[]>()
25101
{
26102
new Type[]{typeof(object)},
27-
})
103+
}),
104+
[nameof(CosmosLinqExtensions.RRF)] = new RRFVisit(),
105+
[nameof(CosmosLinqExtensions.FullTextScore)] = new FullTextScoreVisit(),
28106
};
29107
}
30108

0 commit comments

Comments
 (0)