Skip to content

Commit 5bba9a0

Browse files
leminh98Minh Le (from Dev Box)
andauthored
Query: Adds support for multi-value Group By query for LINQ (#4481)
* init * Added tests * Preliminary code * baseline * add ordering ignore to test cases * upate baseline * address code review 1 * Changed the way comparison between Anonymoustype object worked. Also added handling of multivalue case to call directly to leaf layer visitors, instead of going through the top level scalar expression visitor to avoid changing binding and context scope * addressed code review * address code review * addressed missing field --------- Co-authored-by: Minh Le (from Dev Box) <[email protected]>
1 parent 7867f54 commit 5bba9a0

File tree

5 files changed

+371
-126
lines changed

5 files changed

+371
-126
lines changed

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

Lines changed: 179 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,29 @@ private static SqlObjectProperty[] CreateInitializers(ReadOnlyCollection<Express
884884
result[i] = prop;
885885
}
886886

887+
return result;
888+
}
889+
890+
private static SqlSelectItem[] CreateSelectItems(ReadOnlyCollection<Expression> arguments, ReadOnlyCollection<MemberInfo> members, TranslationContext context)
891+
{
892+
if (arguments.Count != members.Count)
893+
{
894+
throw new InvalidOperationException("Expected same number of arguments as members");
895+
}
896+
897+
SqlSelectItem[] result = new SqlSelectItem[arguments.Count];
898+
for (int i = 0; i < arguments.Count; i++)
899+
{
900+
Expression arg = arguments[i];
901+
MemberInfo member = members[i];
902+
SqlScalarExpression selectExpression = ExpressionToSql.VisitScalarExpression(arg, context);
903+
904+
string memberName = member.GetMemberName(context);
905+
SqlIdentifier alias = SqlIdentifier.Create(memberName);
906+
SqlSelectItem prop = SqlSelectItem.Create(selectExpression, alias);
907+
result[i] = prop;
908+
}
909+
887910
return result;
888911
}
889912

@@ -1314,6 +1337,80 @@ private static Collection VisitMethodCall(MethodCallExpression inputExpression,
13141337
context.PopMethod();
13151338
return result;
13161339
}
1340+
1341+
/// <summary>
1342+
/// Visit a method call, construct the corresponding query and return the select clause for the aggregate function.
1343+
/// At ExpressionToSql point only LINQ method calls are allowed.
1344+
/// These methods are static extension methods of IQueryable or IEnumerable.
1345+
/// </summary>
1346+
/// <param name="inputExpression">Method to translate.</param>
1347+
/// <param name="context">Query translation context.</param>
1348+
private static SqlSelectClause VisitGroupByAggregateMethodCall(MethodCallExpression inputExpression, TranslationContext context)
1349+
{
1350+
context.PushMethod(inputExpression);
1351+
1352+
Type declaringType = inputExpression.Method.DeclaringType;
1353+
if ((declaringType != typeof(Queryable) && declaringType != typeof(Enumerable))
1354+
|| !inputExpression.Method.IsStatic)
1355+
{
1356+
throw new DocumentQueryException(string.Format(CultureInfo.CurrentCulture, ClientResources.OnlyLINQMethodsAreSupported, inputExpression.Method.Name));
1357+
}
1358+
1359+
if (inputExpression.Object != null)
1360+
{
1361+
throw new DocumentQueryException(ClientResources.ExpectedMethodCallsMethods);
1362+
}
1363+
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+
1372+
if (context.LastExpressionIsGroupBy)
1373+
{
1374+
throw new DocumentQueryException(string.Format(CultureInfo.CurrentCulture, "Group By cannot be followed by other methods"));
1375+
}
1376+
1377+
SqlSelectClause select;
1378+
switch (inputExpression.Method.Name)
1379+
{
1380+
case LinqMethods.Average:
1381+
{
1382+
select = ExpressionToSql.VisitAggregateFunction(inputExpression.Arguments, context, SqlFunctionCallScalarExpression.Names.Avg);
1383+
break;
1384+
}
1385+
case LinqMethods.Count:
1386+
{
1387+
select = ExpressionToSql.VisitCount(inputExpression.Arguments, context);
1388+
break;
1389+
}
1390+
case LinqMethods.Max:
1391+
{
1392+
select = ExpressionToSql.VisitAggregateFunction(inputExpression.Arguments, context, SqlFunctionCallScalarExpression.Names.Max);
1393+
break;
1394+
}
1395+
case LinqMethods.Min:
1396+
{
1397+
select = ExpressionToSql.VisitAggregateFunction(inputExpression.Arguments, context, SqlFunctionCallScalarExpression.Names.Min);
1398+
break;
1399+
}
1400+
case LinqMethods.Sum:
1401+
{
1402+
select = ExpressionToSql.VisitAggregateFunction(inputExpression.Arguments, context, SqlFunctionCallScalarExpression.Names.Sum);
1403+
break;
1404+
}
1405+
default:
1406+
throw new DocumentQueryException(string.Format(CultureInfo.CurrentCulture, ClientResources.MethodNotSupported, inputExpression.Method.Name));
1407+
}
1408+
1409+
context.PopSubqueryBinding();
1410+
context.PopCollection();
1411+
context.PopMethod();
1412+
return select;
1413+
}
13171414

13181415
/// <summary>
13191416
/// Determine if an expression should be translated to a subquery.
@@ -1735,48 +1832,93 @@ private static Collection VisitGroupBy(Type returnElementType, ReadOnlyCollectio
17351832
switch (valueSelectorExpression.NodeType)
17361833
{
17371834
case ExpressionType.Constant:
1738-
{
1739-
ConstantExpression constantExpression = (ConstantExpression)valueSelectorExpression;
1740-
SqlScalarExpression selectExpression = ExpressionToSql.VisitConstant(constantExpression, context);
1741-
1742-
SqlSelectSpec sqlSpec = SqlSelectValueSpec.Create(selectExpression);
1743-
SqlSelectClause select = SqlSelectClause.Create(sqlSpec, null);
1744-
context.CurrentQuery = context.CurrentQuery.AddSelectClause(select, context);
1745-
break;
1746-
}
1835+
{
1836+
ConstantExpression constantExpression = (ConstantExpression)valueSelectorExpression;
1837+
SqlScalarExpression selectExpression = ExpressionToSql.VisitConstant(constantExpression, context);
1838+
1839+
SqlSelectSpec sqlSpec = SqlSelectValueSpec.Create(selectExpression);
1840+
SqlSelectClause select = SqlSelectClause.Create(sqlSpec, null);
1841+
context.CurrentQuery = context.CurrentQuery.AddSelectClause(select, context);
1842+
break;
1843+
}
17471844
case ExpressionType.Parameter:
1748-
{
1749-
ParameterExpression parameterValueExpression = (ParameterExpression)valueSelectorExpression;
1750-
SqlScalarExpression selectExpression = ExpressionToSql.VisitParameter(parameterValueExpression, context);
1845+
{
1846+
ParameterExpression parameterValueExpression = (ParameterExpression)valueSelectorExpression;
1847+
SqlScalarExpression selectExpression = ExpressionToSql.VisitParameter(parameterValueExpression, context);
17511848

1752-
SqlSelectSpec sqlSpec = SqlSelectValueSpec.Create(selectExpression);
1753-
SqlSelectClause select = SqlSelectClause.Create(sqlSpec, null);
1754-
context.CurrentQuery = context.CurrentQuery.AddSelectClause(select, context);
1755-
break;
1756-
}
1849+
SqlSelectSpec sqlSpec = SqlSelectValueSpec.Create(selectExpression);
1850+
SqlSelectClause select = SqlSelectClause.Create(sqlSpec, null);
1851+
context.CurrentQuery = context.CurrentQuery.AddSelectClause(select, context);
1852+
break;
1853+
}
17571854
case ExpressionType.Call:
1758-
{
1759-
// Single Value Selector
1760-
MethodCallExpression methodCallExpression = (MethodCallExpression)valueSelectorExpression;
1761-
switch (methodCallExpression.Method.Name)
17621855
{
1763-
case LinqMethods.Max:
1764-
case LinqMethods.Min:
1765-
case LinqMethods.Average:
1766-
case LinqMethods.Count:
1767-
case LinqMethods.Sum:
1768-
ExpressionToSql.VisitMethodCall(methodCallExpression, context);
1769-
break;
1770-
default:
1771-
throw new DocumentQueryException(string.Format(CultureInfo.CurrentCulture, ClientResources.MethodNotSupported, methodCallExpression.Method.Name));
1772-
}
1856+
// Single Value Selector
1857+
MethodCallExpression methodCallExpression = (MethodCallExpression)valueSelectorExpression;
1858+
SqlSelectClause select = ExpressionToSql.VisitGroupByAggregateMethodCall(methodCallExpression, context);
1859+
context.CurrentQuery = context.CurrentQuery.AddSelectClause(select, context);
1860+
break;
1861+
}
1862+
case ExpressionType.New:
1863+
{
1864+
// Add select item clause at the end of this method
1865+
NewExpression newExpression = (NewExpression)valueSelectorExpression;
17731866

1774-
break;
1775-
}
1776-
case ExpressionType.New:
1777-
// TODO: Multi Value Selector
1778-
throw new DocumentQueryException(string.Format(CultureInfo.CurrentCulture, ClientResources.ExpressionTypeIsNotSupported, ExpressionType.New));
1779-
1867+
if (newExpression.Members == null)
1868+
{
1869+
throw new DocumentQueryException(ClientResources.ConstructorInvocationNotSupported);
1870+
}
1871+
1872+
// Get the list of items and the bindings
1873+
ReadOnlyCollection<Expression> newExpressionArguments = newExpression.Arguments;
1874+
ReadOnlyCollection<MemberInfo> newExpressionMembers = newExpression.Members;
1875+
1876+
SqlSelectItem[] selectItems = new SqlSelectItem[newExpressionArguments.Count];
1877+
for (int i = 0; i < newExpressionArguments.Count; i++)
1878+
{
1879+
MemberInfo member = newExpressionMembers[i];
1880+
string memberName = member.GetMemberName(context);
1881+
SqlIdentifier alias = SqlIdentifier.Create(memberName);
1882+
1883+
Expression arg = newExpressionArguments[i];
1884+
switch (arg.NodeType)
1885+
{
1886+
case ExpressionType.Constant:
1887+
{
1888+
SqlScalarExpression selectExpression = ExpressionToSql.VisitConstant((ConstantExpression)arg, context);
1889+
1890+
SqlSelectItem prop = SqlSelectItem.Create(selectExpression, alias);
1891+
selectItems[i] = prop;
1892+
break;
1893+
}
1894+
case ExpressionType.Parameter:
1895+
{
1896+
SqlScalarExpression selectExpression = ExpressionToSql.VisitParameter((ParameterExpression)arg, context);
1897+
1898+
SqlSelectItem prop = SqlSelectItem.Create(selectExpression, alias);
1899+
selectItems[i] = prop;
1900+
break;
1901+
}
1902+
case ExpressionType.Call:
1903+
{
1904+
SqlSelectClause selectClause = ExpressionToSql.VisitGroupByAggregateMethodCall((MethodCallExpression)arg, context);
1905+
SqlScalarExpression selectExpression = ((SqlSelectValueSpec)selectClause.SelectSpec).Expression;
1906+
1907+
SqlSelectItem prop = SqlSelectItem.Create(selectExpression, alias);
1908+
selectItems[i] = prop;
1909+
break;
1910+
}
1911+
default:
1912+
throw new DocumentQueryException(string.Format(CultureInfo.CurrentCulture, ClientResources.ExpressionTypeIsNotSupported, arg.NodeType));
1913+
}
1914+
}
1915+
1916+
SqlSelectListSpec sqlSpec = SqlSelectListSpec.Create(selectItems);
1917+
SqlSelectClause select = SqlSelectClause.Create(sqlSpec, null);
1918+
context.CurrentQuery = context.CurrentQuery.AddSelectClause(select, context);
1919+
1920+
break;
1921+
}
17801922
default:
17811923
throw new DocumentQueryException(string.Format(CultureInfo.CurrentCulture, ClientResources.ExpressionTypeIsNotSupported, valueSelectorExpression.NodeType));
17821924
}

0 commit comments

Comments
 (0)