Skip to content

Commit 24b8ca6

Browse files
leminh98leminh98
andauthored
Query: Adds LINQ support for Multi-key Group By translation (#4857)
# Pull Request Template ## Description This PR features the new syntax for multi-key group by queries, allowing for grouping of multiple properties. Full syntax: ```CSharp // Function Signature public static System.Collections.Generic.IEnumerable<System.Linq.IGrouping<TKey,TElement>> GroupBy<TSource,TKey,TElement> (this System.Collections.Generic.IEnumerable<TSource> source, Func<TSource,TKey> keySelector, Func<TSource,TElement> elementSelector); // Usage Enumerables.GroupBy( groupByKeys => keySelectiorLambda, (keys, values) => valueSelectorLambda); ``` Example - Grouping by single keys and projecting single value ```Csharp Enumerable.GroupBy( /*keySelector*/ k => k.Id, /*valueSelector*/ (key, values) => values.Count()); ``` - Grouping by single keys and projecting multiple values ```Csharp Enumerable.GroupBy( /*keySelector*/ k => k.Id, /*valueSelector*/ (key, values) => new { idField = key.key1, count = values.Count(), }); ``` - Grouping by multiple keys and projecting single value ```Csharp Enumerable.GroupBy( /*keySelector*/ k => new { key1 = k.Id, key2 = k.Int } , /*valueSelector*/ (key, values) => values.Count()); ``` - Grouping by multiple keys and projecting multiple values ```Csharp Enumerable.GroupBy( /*keySelector*/ k => new { key1 = k.Id, key2 = k.Int } , /*valueSelector*/ (key, values) => new { idField = key.key1, count = values.Count(), }) ``` ## 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 closes ##1202 --------- Co-authored-by: leminh98 <[email protected]>
1 parent 9fa85ee commit 24b8ca6

File tree

8 files changed

+537
-228
lines changed

8 files changed

+537
-228
lines changed

Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs

Lines changed: 152 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ namespace Microsoft.Azure.Cosmos.Linq
1515
using System.Linq.Expressions;
1616
using System.Reflection;
1717
using System.Text.RegularExpressions;
18-
using Microsoft.Azure.Cosmos.CosmosElements;
18+
using Microsoft.Azure.Cosmos.CosmosElements;
19+
using Microsoft.Azure.Cosmos.Query.Core.ClientDistributionPlan.Cql;
1920
using Microsoft.Azure.Cosmos.Serialization.HybridRow;
2021
using Microsoft.Azure.Cosmos.Serializer;
2122
using Microsoft.Azure.Cosmos.Spatial;
@@ -1361,14 +1362,6 @@ private static SqlSelectClause VisitGroupByAggregateMethodCall(MethodCallExpress
13611362
throw new DocumentQueryException(ClientResources.ExpectedMethodCallsMethods);
13621363
}
13631364

1364-
Expression inputCollection = inputExpression.Arguments[0]; // all these methods are static extension methods, so argument[0] is the collection
1365-
1366-
Collection collection = ExpressionToSql.Translate(inputCollection, context);
1367-
context.PushCollection(collection);
1368-
1369-
bool shouldBeOnNewQuery = context.CurrentQuery.ShouldBeOnNewQuery(inputExpression.Method.Name, inputExpression.Arguments.Count);
1370-
context.PushSubqueryBinding(shouldBeOnNewQuery);
1371-
13721365
if (context.LastExpressionIsGroupBy)
13731366
{
13741367
throw new DocumentQueryException(string.Format(CultureInfo.CurrentCulture, "Group By cannot be followed by other methods"));
@@ -1406,8 +1399,6 @@ private static SqlSelectClause VisitGroupByAggregateMethodCall(MethodCallExpress
14061399
throw new DocumentQueryException(string.Format(CultureInfo.CurrentCulture, ClientResources.MethodNotSupported, inputExpression.Method.Name));
14071400
}
14081401

1409-
context.PopSubqueryBinding();
1410-
context.PopCollection();
14111402
context.PopMethod();
14121403
return select;
14131404
}
@@ -1793,44 +1784,111 @@ private static Collection VisitGroupBy(Type returnElementType, ReadOnlyCollectio
17931784
{
17941785
throw new DocumentQueryException(string.Format(CultureInfo.CurrentCulture, ClientResources.InvalidArgumentsCount, LinqMethods.GroupBy, 3, arguments.Count));
17951786
}
1796-
1797-
// bind the parameters in the value selector to the current input
1798-
foreach (ParameterExpression par in Utilities.GetLambda(arguments[2]).Parameters)
1799-
{
1800-
context.PushParameter(par, context.CurrentSubqueryBinding.ShouldBeOnNewQuery);
1801-
}
1802-
1787+
1788+
// Key Selector handling
18031789
// First argument is input, second is key selector and third is value selector
1804-
LambdaExpression keySelectorLambda = Utilities.GetLambda(arguments[1]);
1805-
1806-
// Current GroupBy doesn't allow subquery, so we need to visit non subquery scalar lambda
1807-
SqlScalarExpression keySelectorFunc = ExpressionToSql.VisitNonSubqueryScalarLambda(keySelectorLambda, context);
1808-
1809-
SqlGroupByClause groupby = SqlGroupByClause.Create(keySelectorFunc);
1810-
1811-
context.CurrentQuery = context.CurrentQuery.AddGroupByClause(groupby, context);
1812-
1813-
// Create a GroupBy collection and bind the new GroupBy collection to the new parameters created from the key
1814-
Collection collection = ExpressionToSql.ConvertToCollection(keySelectorFunc);
1815-
collection.isOuter = true;
1816-
collection.Name = "GroupBy";
1817-
1818-
ParameterExpression parameterExpression = context.GenerateFreshParameter(returnElementType, keySelectorFunc.ToString(), includeSuffix: false);
1819-
Binding binding = new Binding(parameterExpression, collection.inner, isInCollection: false, isInputParameter: true);
1820-
1821-
context.CurrentQuery.GroupByParameter = new FromParameterBindings();
1822-
context.CurrentQuery.GroupByParameter.Add(binding);
1823-
1824-
// The alias for the key in the value selector lambda is the first arguemt lambda - we bound it to the parameter expression, which already has substitution
1825-
ParameterExpression valueSelectorKeyExpressionAlias = Utilities.GetLambda(arguments[2]).Parameters[0];
1826-
context.GroupByKeySubstitution.AddSubstitution(valueSelectorKeyExpressionAlias, parameterExpression/*Utilities.GetLambda(arguments[1]).Body*/);
1827-
1828-
// Translate the body of the value selector lambda
1790+
LambdaExpression keySelectorLambda = Utilities.GetLambda(arguments[1]);
1791+
1792+
Collection collection = new Collection("Group By");
1793+
context.CurrentQuery.GroupByParameter = new FromParameterBindings();
1794+
1795+
SqlGroupByClause groupby;
1796+
ParameterExpression parameterExpression;
1797+
switch (keySelectorLambda.Body.NodeType)
1798+
{
1799+
case ExpressionType.Parameter:
1800+
case ExpressionType.Call:
1801+
case ExpressionType.MemberAccess:
1802+
{
1803+
// bind the parameters in the value selector to the current input
1804+
foreach (ParameterExpression par in Utilities.GetLambda(arguments[2]).Parameters)
1805+
{
1806+
context.PushParameter(par, context.CurrentSubqueryBinding.ShouldBeOnNewQuery);
1807+
}
1808+
1809+
//Current GroupBy doesn't allow subquery, so we need to visit non subquery scalar lambda
1810+
SqlScalarExpression keySelectorFunc = ExpressionToSql.VisitNonSubqueryScalarLambda(keySelectorLambda, context);
1811+
1812+
// The group by clause don't need to handle the value selector, so adding the clause to the uery now.
1813+
groupby = SqlGroupByClause.Create(keySelectorFunc);
1814+
parameterExpression = context.GenerateFreshParameter(returnElementType, keySelectorFunc.ToString(), includeSuffix: false);
1815+
1816+
break;
1817+
}
1818+
case ExpressionType.New:
1819+
{
1820+
// bind the parameters in the key selector to the current input - in this case, the value selector key is being substituted by the key selector
1821+
foreach (ParameterExpression par in Utilities.GetLambda(arguments[1]).Parameters)
1822+
{
1823+
context.PushParameter(par, context.CurrentSubqueryBinding.ShouldBeOnNewQuery);
1824+
}
1825+
1826+
NewExpression newExpression = (NewExpression)keySelectorLambda.Body;
1827+
1828+
if (newExpression.Members == null)
1829+
{
1830+
throw new DocumentQueryException(ClientResources.ConstructorInvocationNotSupported);
1831+
}
1832+
1833+
ReadOnlyCollection<Expression> newExpressionArguments = newExpression.Arguments;
1834+
1835+
List<SqlScalarExpression> keySelectorFunctions = new List<SqlScalarExpression>();
1836+
for (int i = 0; i < newExpressionArguments.Count; i++)
1837+
{
1838+
//Current GroupBy doesn't allow subquery, so we need to visit non subquery scalara
1839+
SqlScalarExpression keySelectorFunc = ExpressionToSql.VisitNonSubqueryScalarExpression(newExpressionArguments[i], context);
1840+
keySelectorFunctions.Add(keySelectorFunc);
1841+
}
1842+
1843+
groupby = SqlGroupByClause.Create(keySelectorFunctions.ToImmutableArray());
1844+
parameterExpression = context.GenerateFreshParameter(returnElementType, keySelectorFunctions.ToString(), includeSuffix: false);
1845+
1846+
break;
1847+
}
1848+
default:
1849+
throw new DocumentQueryException(string.Format(CultureInfo.CurrentCulture, ClientResources.ExpressionTypeIsNotSupported, keySelectorLambda.Body.NodeType));
1850+
}
1851+
1852+
// The group by clause don't need to handle the value selector, so adding the clause to the qery now.
1853+
context.CurrentQuery = context.CurrentQuery.AddGroupByClause(groupby, context);
1854+
1855+
// Bind the alias
1856+
Binding binding = new Binding(parameterExpression, collection.inner, isInCollection: false, isInputParameter: true);
1857+
context.CurrentQuery.GroupByParameter.Add(binding);
1858+
1859+
// The alias for the key in the value selector lambda is the first arguemt lambda - we bound it to the parameter expression, which already has substitution
1860+
ParameterExpression valueSelectorKeyExpressionAlias = Utilities.GetLambda(arguments[2]).Parameters[0];
1861+
context.GroupByKeySubstitution.AddSubstitution(valueSelectorKeyExpressionAlias, parameterExpression);
1862+
1863+
// Value Selector Handingling
1864+
// Translate the body of the value selector lambda
18291865
Expression valueSelectorExpression = Utilities.GetLambda(arguments[2]).Body;
18301866

18311867
// The value selector function needs to be either a MethodCall or an AnonymousType
18321868
switch (valueSelectorExpression.NodeType)
1833-
{
1869+
{
1870+
case ExpressionType.MemberAccess:
1871+
{
1872+
MemberExpression memberAccessExpression = (MemberExpression)valueSelectorExpression;
1873+
1874+
if (memberAccessExpression.Expression.NodeType == ExpressionType.Parameter)
1875+
{
1876+
// Look up the object of the expression to see if it is the key
1877+
ParameterExpression memberAccessObject = (ParameterExpression)memberAccessExpression.Expression;
1878+
Expression subst = context.GroupByKeySubstitution.Lookup(memberAccessObject);
1879+
if (subst != null)
1880+
{
1881+
// If there is a match, we construct a new Member Access expression with the substituted expression and visit it to create a select clause
1882+
MemberExpression newMemberAccessExpression = memberAccessExpression.Update(keySelectorLambda.Body);
1883+
SqlScalarExpression selectExpression = ExpressionToSql.VisitMemberAccess(newMemberAccessExpression, context);
1884+
1885+
SqlSelectSpec sqlSpec = SqlSelectValueSpec.Create(selectExpression);
1886+
SqlSelectClause select = SqlSelectClause.Create(sqlSpec, null);
1887+
context.CurrentQuery = context.CurrentQuery.AddSelectClause(select, context);
1888+
}
1889+
}
1890+
break;
1891+
}
18341892
case ExpressionType.Constant:
18351893
{
18361894
ConstantExpression constantExpression = (ConstantExpression)valueSelectorExpression;
@@ -1908,6 +1966,27 @@ private static Collection VisitGroupBy(Type returnElementType, ReadOnlyCollectio
19081966
selectItems[i] = prop;
19091967
break;
19101968
}
1969+
case ExpressionType.MemberAccess:
1970+
{
1971+
MemberExpression memberAccessExpression = (MemberExpression)arg;
1972+
1973+
if (memberAccessExpression.Expression.NodeType == ExpressionType.Parameter)
1974+
{
1975+
// Look up the object of the expression to see if it is the key
1976+
ParameterExpression memberAccessObject = (ParameterExpression)memberAccessExpression.Expression;
1977+
Expression subst = context.GroupByKeySubstitution.Lookup(memberAccessObject);
1978+
if (subst != null)
1979+
{
1980+
// If there is a match, we construct a new Member Access expression with the substituted expression and visit it to create a select clause
1981+
MemberExpression newMemberAccessExpression = memberAccessExpression.Update(keySelectorLambda.Body); /*System.Linq.Expressions.Expression.Field(subst, memberAccessExpression.Member.Name);*/
1982+
SqlScalarExpression selectExpression = ExpressionToSql.VisitMemberAccess(newMemberAccessExpression, context);
1983+
1984+
SqlSelectItem prop = SqlSelectItem.Create(selectExpression, alias);
1985+
selectItems[i] = prop;
1986+
}
1987+
}
1988+
break;
1989+
}
19111990
default:
19121991
throw new DocumentQueryException(string.Format(CultureInfo.CurrentCulture, ClientResources.ExpressionTypeIsNotSupported, arg.NodeType));
19131992
}
@@ -1921,13 +2000,34 @@ private static Collection VisitGroupBy(Type returnElementType, ReadOnlyCollectio
19212000
}
19222001
default:
19232002
throw new DocumentQueryException(string.Format(CultureInfo.CurrentCulture, ClientResources.ExpressionTypeIsNotSupported, valueSelectorExpression.NodeType));
1924-
}
1925-
1926-
foreach (ParameterExpression par in Utilities.GetLambda(arguments[2]).Parameters)
1927-
{
1928-
context.PopParameter();
1929-
}
1930-
2003+
}
2004+
2005+
// Pop the correct number of items off the parameter stack
2006+
switch (keySelectorLambda.Body.NodeType)
2007+
{
2008+
case ExpressionType.Parameter:
2009+
case ExpressionType.Call:
2010+
case ExpressionType.MemberAccess:
2011+
{
2012+
foreach (ParameterExpression param in Utilities.GetLambda(arguments[2]).Parameters)
2013+
{
2014+
context.PopParameter();
2015+
}
2016+
break;
2017+
}
2018+
case ExpressionType.New:
2019+
{
2020+
//bind the parameters in the value selector to the current input
2021+
foreach (ParameterExpression param in Utilities.GetLambda(arguments[1]).Parameters)
2022+
{
2023+
context.PopParameter();
2024+
}
2025+
break;
2026+
}
2027+
default:
2028+
break;
2029+
}
2030+
19312031
return collection;
19322032
}
19332033

@@ -2234,7 +2334,7 @@ private static SqlInputPathCollection ConvertMemberIndexerToPath(SqlMemberIndexe
22342334
if (parent == null)
22352335
{
22362336
break;
2237-
}
2337+
}
22382338

22392339
if (parent is SqlPropertyRefScalarExpression sqlPropertyRefScalarExpression)
22402340
{

Microsoft.Azure.Cosmos/src/Linq/QueryUnderConstruction.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -250,8 +250,8 @@ public QueryUnderConstruction FlattenAsPossible()
250250
break;
251251
}
252252

253-
// In case of Select -> Group by cases, the Select query should not be flattened and kept as a subquery
254-
if ((query.inputQuery?.selectClause != null) && (query.groupByClause != null))
253+
// In case of Select/Order By -> Group by cases, the Select/Order By query should not be flattened and kept as a subquery
254+
if (((query.inputQuery?.selectClause != null) || (query.inputQuery?.orderByClause != null)) && (query.groupByClause != null))
255255
{
256256
flattenQuery = this;
257257
break;
@@ -564,11 +564,12 @@ public bool ShouldBeOnNewQuery(string methodName, int argumentCount)
564564
break;
565565

566566
case LinqMethods.GroupBy:
567-
// New query is needed when there is already a Take or a Select or a Group by clause
567+
// New query is needed when there is already a Take or a Select or a Group by clause or an Order By clause
568568
shouldPackage = (this.topSpec != null) ||
569569
(this.offsetSpec != null) ||
570570
(this.selectClause != null) ||
571-
(this.groupByClause != null);
571+
(this.groupByClause != null) ||
572+
(this.orderByClause != null);
572573
break;
573574

574575
case LinqMethods.Skip:

Microsoft.Azure.Cosmos/src/Linq/TranslationContext.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,12 +232,12 @@ public void PushCollection(Collection collection)
232232
throw new ArgumentNullException("collection");
233233
}
234234

235-
if (this.CurrentQuery.GroupByParameter == null) this.collectionStack.Add(collection);
235+
this.collectionStack.Add(collection);
236236
}
237237

238238
public void PopCollection()
239239
{
240-
if (this.CurrentQuery.GroupByParameter == null) this.collectionStack.RemoveAt(this.collectionStack.Count - 1);
240+
this.collectionStack.RemoveAt(this.collectionStack.Count - 1);
241241
}
242242

243243
/// <summary>

0 commit comments

Comments
 (0)