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) <leminh@microsoft.com>
This commit is contained in:
leminh98 2024-07-08 21:07:50 -07:00 коммит произвёл GitHub
Родитель 7867f549e8
Коммит 5bba9a05b0
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
5 изменённых файлов: 373 добавлений и 128 удалений

Просмотреть файл

@ -884,6 +884,29 @@ namespace Microsoft.Azure.Cosmos.Linq
result[i] = prop;
}
return result;
}
private static SqlSelectItem[] CreateSelectItems(ReadOnlyCollection<Expression> arguments, ReadOnlyCollection<MemberInfo> members, TranslationContext context)
{
if (arguments.Count != members.Count)
{
throw new InvalidOperationException("Expected same number of arguments as members");
}
SqlSelectItem[] result = new SqlSelectItem[arguments.Count];
for (int i = 0; i < arguments.Count; i++)
{
Expression arg = arguments[i];
MemberInfo member = members[i];
SqlScalarExpression selectExpression = ExpressionToSql.VisitScalarExpression(arg, context);
string memberName = member.GetMemberName(context);
SqlIdentifier alias = SqlIdentifier.Create(memberName);
SqlSelectItem prop = SqlSelectItem.Create(selectExpression, alias);
result[i] = prop;
}
return result;
}
@ -1314,6 +1337,80 @@ namespace Microsoft.Azure.Cosmos.Linq
context.PopMethod();
return result;
}
/// <summary>
/// Visit a method call, construct the corresponding query and return the select clause for the aggregate function.
/// At ExpressionToSql point only LINQ method calls are allowed.
/// These methods are static extension methods of IQueryable or IEnumerable.
/// </summary>
/// <param name="inputExpression">Method to translate.</param>
/// <param name="context">Query translation context.</param>
private static SqlSelectClause VisitGroupByAggregateMethodCall(MethodCallExpression inputExpression, TranslationContext context)
{
context.PushMethod(inputExpression);
Type declaringType = inputExpression.Method.DeclaringType;
if ((declaringType != typeof(Queryable) && declaringType != typeof(Enumerable))
|| !inputExpression.Method.IsStatic)
{
throw new DocumentQueryException(string.Format(CultureInfo.CurrentCulture, ClientResources.OnlyLINQMethodsAreSupported, inputExpression.Method.Name));
}
if (inputExpression.Object != null)
{
throw new DocumentQueryException(ClientResources.ExpectedMethodCallsMethods);
}
Expression inputCollection = inputExpression.Arguments[0]; // all these methods are static extension methods, so argument[0] is the collection
Collection collection = ExpressionToSql.Translate(inputCollection, context);
context.PushCollection(collection);
bool shouldBeOnNewQuery = context.CurrentQuery.ShouldBeOnNewQuery(inputExpression.Method.Name, inputExpression.Arguments.Count);
context.PushSubqueryBinding(shouldBeOnNewQuery);
if (context.LastExpressionIsGroupBy)
{
throw new DocumentQueryException(string.Format(CultureInfo.CurrentCulture, "Group By cannot be followed by other methods"));
}
SqlSelectClause select;
switch (inputExpression.Method.Name)
{
case LinqMethods.Average:
{
select = ExpressionToSql.VisitAggregateFunction(inputExpression.Arguments, context, SqlFunctionCallScalarExpression.Names.Avg);
break;
}
case LinqMethods.Count:
{
select = ExpressionToSql.VisitCount(inputExpression.Arguments, context);
break;
}
case LinqMethods.Max:
{
select = ExpressionToSql.VisitAggregateFunction(inputExpression.Arguments, context, SqlFunctionCallScalarExpression.Names.Max);
break;
}
case LinqMethods.Min:
{
select = ExpressionToSql.VisitAggregateFunction(inputExpression.Arguments, context, SqlFunctionCallScalarExpression.Names.Min);
break;
}
case LinqMethods.Sum:
{
select = ExpressionToSql.VisitAggregateFunction(inputExpression.Arguments, context, SqlFunctionCallScalarExpression.Names.Sum);
break;
}
default:
throw new DocumentQueryException(string.Format(CultureInfo.CurrentCulture, ClientResources.MethodNotSupported, inputExpression.Method.Name));
}
context.PopSubqueryBinding();
context.PopCollection();
context.PopMethod();
return select;
}
/// <summary>
/// Determine if an expression should be translated to a subquery.
@ -1735,48 +1832,93 @@ namespace Microsoft.Azure.Cosmos.Linq
switch (valueSelectorExpression.NodeType)
{
case ExpressionType.Constant:
{
ConstantExpression constantExpression = (ConstantExpression)valueSelectorExpression;
SqlScalarExpression selectExpression = ExpressionToSql.VisitConstant(constantExpression, context);
SqlSelectSpec sqlSpec = SqlSelectValueSpec.Create(selectExpression);
SqlSelectClause select = SqlSelectClause.Create(sqlSpec, null);
context.CurrentQuery = context.CurrentQuery.AddSelectClause(select, context);
break;
}
case ExpressionType.Parameter:
{
ParameterExpression parameterValueExpression = (ParameterExpression)valueSelectorExpression;
SqlScalarExpression selectExpression = ExpressionToSql.VisitParameter(parameterValueExpression, context);
SqlSelectSpec sqlSpec = SqlSelectValueSpec.Create(selectExpression);
SqlSelectClause select = SqlSelectClause.Create(sqlSpec, null);
context.CurrentQuery = context.CurrentQuery.AddSelectClause(select, context);
break;
}
case ExpressionType.Call:
{
// Single Value Selector
MethodCallExpression methodCallExpression = (MethodCallExpression)valueSelectorExpression;
switch (methodCallExpression.Method.Name)
{
case LinqMethods.Max:
case LinqMethods.Min:
case LinqMethods.Average:
case LinqMethods.Count:
case LinqMethods.Sum:
ExpressionToSql.VisitMethodCall(methodCallExpression, context);
break;
default:
throw new DocumentQueryException(string.Format(CultureInfo.CurrentCulture, ClientResources.MethodNotSupported, methodCallExpression.Method.Name));
}
ConstantExpression constantExpression = (ConstantExpression)valueSelectorExpression;
SqlScalarExpression selectExpression = ExpressionToSql.VisitConstant(constantExpression, context);
break;
}
case ExpressionType.New:
// TODO: Multi Value Selector
throw new DocumentQueryException(string.Format(CultureInfo.CurrentCulture, ClientResources.ExpressionTypeIsNotSupported, ExpressionType.New));
SqlSelectSpec sqlSpec = SqlSelectValueSpec.Create(selectExpression);
SqlSelectClause select = SqlSelectClause.Create(sqlSpec, null);
context.CurrentQuery = context.CurrentQuery.AddSelectClause(select, context);
break;
}
case ExpressionType.Parameter:
{
ParameterExpression parameterValueExpression = (ParameterExpression)valueSelectorExpression;
SqlScalarExpression selectExpression = ExpressionToSql.VisitParameter(parameterValueExpression, context);
SqlSelectSpec sqlSpec = SqlSelectValueSpec.Create(selectExpression);
SqlSelectClause select = SqlSelectClause.Create(sqlSpec, null);
context.CurrentQuery = context.CurrentQuery.AddSelectClause(select, context);
break;
}
case ExpressionType.Call:
{
// Single Value Selector
MethodCallExpression methodCallExpression = (MethodCallExpression)valueSelectorExpression;
SqlSelectClause select = ExpressionToSql.VisitGroupByAggregateMethodCall(methodCallExpression, context);
context.CurrentQuery = context.CurrentQuery.AddSelectClause(select, context);
break;
}
case ExpressionType.New:
{
// Add select item clause at the end of this method
NewExpression newExpression = (NewExpression)valueSelectorExpression;
if (newExpression.Members == null)
{
throw new DocumentQueryException(ClientResources.ConstructorInvocationNotSupported);
}
// Get the list of items and the bindings
ReadOnlyCollection<Expression> newExpressionArguments = newExpression.Arguments;
ReadOnlyCollection<MemberInfo> newExpressionMembers = newExpression.Members;
SqlSelectItem[] selectItems = new SqlSelectItem[newExpressionArguments.Count];
for (int i = 0; i < newExpressionArguments.Count; i++)
{
MemberInfo member = newExpressionMembers[i];
string memberName = member.GetMemberName(context);
SqlIdentifier alias = SqlIdentifier.Create(memberName);
Expression arg = newExpressionArguments[i];
switch (arg.NodeType)
{
case ExpressionType.Constant:
{
SqlScalarExpression selectExpression = ExpressionToSql.VisitConstant((ConstantExpression)arg, context);
SqlSelectItem prop = SqlSelectItem.Create(selectExpression, alias);
selectItems[i] = prop;
break;
}
case ExpressionType.Parameter:
{
SqlScalarExpression selectExpression = ExpressionToSql.VisitParameter((ParameterExpression)arg, context);
SqlSelectItem prop = SqlSelectItem.Create(selectExpression, alias);
selectItems[i] = prop;
break;
}
case ExpressionType.Call:
{
SqlSelectClause selectClause = ExpressionToSql.VisitGroupByAggregateMethodCall((MethodCallExpression)arg, context);
SqlScalarExpression selectExpression = ((SqlSelectValueSpec)selectClause.SelectSpec).Expression;
SqlSelectItem prop = SqlSelectItem.Create(selectExpression, alias);
selectItems[i] = prop;
break;
}
default:
throw new DocumentQueryException(string.Format(CultureInfo.CurrentCulture, ClientResources.ExpressionTypeIsNotSupported, arg.NodeType));
}
}
SqlSelectListSpec sqlSpec = SqlSelectListSpec.Create(selectItems);
SqlSelectClause select = SqlSelectClause.Create(sqlSpec, null);
context.CurrentQuery = context.CurrentQuery.AddSelectClause(select, context);
break;
}
default:
throw new DocumentQueryException(string.Format(CultureInfo.CurrentCulture, ClientResources.ExpressionTypeIsNotSupported, valueSelectorExpression.NodeType));
}

Просмотреть файл

@ -17,8 +17,10 @@ GROUP BY root ]]></SqlQuery>
<Expression><![CDATA[query.GroupBy(k => k.Id, (key, values) => new AnonymousType(Key = key, key = key))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[]]></SqlQuery>
<ErrorMessage><![CDATA[Expression with NodeType 'New' is not supported.]]></ErrorMessage>
<SqlQuery><![CDATA[
SELECT root["id"] AS Key, root["id"] AS key
FROM root
GROUP BY root["id"] ]]></SqlQuery>
</Output>
</Result>
<Result>
@ -27,8 +29,10 @@ GROUP BY root ]]></SqlQuery>
<Expression><![CDATA[query.GroupBy(k => k.Id, (key, values) => new AnonymousType(KeyAlias = key, values = 123))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[]]></SqlQuery>
<ErrorMessage><![CDATA[Expression with NodeType 'New' is not supported.]]></ErrorMessage>
<SqlQuery><![CDATA[
SELECT root["id"] AS KeyAlias, 123 AS values
FROM root
GROUP BY root["id"] ]]></SqlQuery>
</Output>
</Result>
<Result>
@ -37,8 +41,10 @@ GROUP BY root ]]></SqlQuery>
<Expression><![CDATA[query.GroupBy(k => k.Id, (key, values) => new AnonymousType(Min = values.Min(value => value.Int), Max = values.Max(value => value.Int), Avg = values.Average(value => value.Int), Count = values.Count()))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[]]></SqlQuery>
<ErrorMessage><![CDATA[Expression with NodeType 'New' is not supported.]]></ErrorMessage>
<SqlQuery><![CDATA[
SELECT MIN(root["Int"]) AS Min, MAX(root["Int"]) AS Max, AVG(root["Int"]) AS Avg, COUNT(1) AS Count
FROM root
GROUP BY root["id"] ]]></SqlQuery>
</Output>
</Result>
<Result>
@ -47,8 +53,10 @@ GROUP BY root ]]></SqlQuery>
<Expression><![CDATA[query.GroupBy(k => k.FamilyId, (key, values) => new AnonymousType(familyId = key, familyIdCount = values.Count()))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[]]></SqlQuery>
<ErrorMessage><![CDATA[Expression with NodeType 'New' is not supported.]]></ErrorMessage>
<SqlQuery><![CDATA[
SELECT root["FamilyId"] AS familyId, COUNT(1) AS familyIdCount
FROM root
GROUP BY root["FamilyId"] ]]></SqlQuery>
</Output>
</Result>
<Result>
@ -57,8 +65,10 @@ GROUP BY root ]]></SqlQuery>
<Expression><![CDATA[query.GroupBy(k => k.Id, (key, values) => new AnonymousType(Min = values.Min(), Max = values.Max()))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[]]></SqlQuery>
<ErrorMessage><![CDATA[Expression with NodeType 'New' is not supported.]]></ErrorMessage>
<SqlQuery><![CDATA[
SELECT MIN(root) AS Min, MAX(root) AS Max
FROM root
GROUP BY root["id"] ]]></SqlQuery>
</Output>
</Result>
<Result>
@ -68,7 +78,7 @@ GROUP BY root ]]></SqlQuery>
</Input>
<Output>
<SqlQuery><![CDATA[]]></SqlQuery>
<ErrorMessage><![CDATA[Expression with NodeType 'New' is not supported.]]></ErrorMessage>
<ErrorMessage><![CDATA[Method 'Select' is not supported.]]></ErrorMessage>
</Output>
</Result>
<Result>
@ -77,8 +87,13 @@ GROUP BY root ]]></SqlQuery>
<Expression><![CDATA[query.Select(x => x.Id).GroupBy(k => k, (key, values) => new AnonymousType(keyAlias = key, count = values.Count()))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[]]></SqlQuery>
<ErrorMessage><![CDATA[Expression with NodeType 'New' is not supported.]]></ErrorMessage>
<SqlQuery><![CDATA[
SELECT r0 AS keyAlias, COUNT(1) AS count
FROM (
SELECT VALUE root["id"]
FROM root) AS r0
GROUP BY r0
]]></SqlQuery>
</Output>
</Result>
<Result>
@ -87,8 +102,13 @@ GROUP BY root ]]></SqlQuery>
<Expression><![CDATA[query.Select(x => new AnonymousType(Id1 = x.Id, family1 = x.FamilyId, childrenN1 = x.Children)).GroupBy(k => k.Id1, (key, values) => new AnonymousType(keyAlias = key, count = values.Count()))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[]]></SqlQuery>
<ErrorMessage><![CDATA[Expression with NodeType 'New' is not supported.]]></ErrorMessage>
<SqlQuery><![CDATA[
SELECT r0["Id1"] AS keyAlias, COUNT(1) AS count
FROM (
SELECT VALUE {"Id1": root["id"], "family1": root["FamilyId"], "childrenN1": root["Children"]}
FROM root) AS r0
GROUP BY r0["Id1"]
]]></SqlQuery>
</Output>
</Result>
<Result>
@ -97,8 +117,11 @@ GROUP BY root ]]></SqlQuery>
<Expression><![CDATA[query.SelectMany(x => x.Children).GroupBy(k => k, (key, values) => new AnonymousType(keyAlias = key, count = values.Count()))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[]]></SqlQuery>
<ErrorMessage><![CDATA[Expression with NodeType 'New' is not supported.]]></ErrorMessage>
<SqlQuery><![CDATA[
SELECT x0 AS keyAlias, COUNT(1) AS count
FROM root
JOIN x0 IN root["Children"]
GROUP BY x0 ]]></SqlQuery>
</Output>
</Result>
<Result>
@ -107,8 +130,15 @@ GROUP BY root ]]></SqlQuery>
<Expression><![CDATA[query.Skip(10).GroupBy(k => k.Id, (key, values) => new AnonymousType(keyAlias = key, count = values.Count()))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[]]></SqlQuery>
<ErrorMessage><![CDATA[Expression with NodeType 'New' is not supported.]]></ErrorMessage>
<SqlQuery><![CDATA[
SELECT r0["id"] AS keyAlias, COUNT(1) AS count
FROM (
SELECT VALUE root
FROM root
OFFSET 10 LIMIT 2147483647) AS r0
GROUP BY r0["id"]
]]></SqlQuery>
<ErrorMessage><![CDATA[Status Code: BadRequest,{"errors":[{"severity":"Error","location":{"start":81,"end":107},"code":"SC2204","message":"'OFFSET LIMIT' clause is not supported in subqueries."}]},0x800A0B00]]></ErrorMessage>
</Output>
</Result>
<Result>
@ -117,8 +147,14 @@ GROUP BY root ]]></SqlQuery>
<Expression><![CDATA[query.Take(10).GroupBy(k => k.Id, (key, values) => new AnonymousType(keyAlias = key, count = values.Count()))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[]]></SqlQuery>
<ErrorMessage><![CDATA[Expression with NodeType 'New' is not supported.]]></ErrorMessage>
<SqlQuery><![CDATA[
SELECT r0["id"] AS keyAlias, COUNT(1) AS count
FROM (
SELECT TOP 10 VALUE root
FROM root) AS r0
GROUP BY r0["id"]
]]></SqlQuery>
<ErrorMessage><![CDATA[Status Code: BadRequest,{"errors":[{"severity":"Error","location":{"start":60,"end":66},"code":"SC2203","message":"'TOP' is not supported in subqueries."}]},0x800A0B00]]></ErrorMessage>
</Output>
</Result>
<Result>
@ -127,8 +163,15 @@ GROUP BY root ]]></SqlQuery>
<Expression><![CDATA[query.Skip(10).Take(10).GroupBy(k => k.Id, (key, values) => new AnonymousType(keyAlias = key, count = values.Count()))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[]]></SqlQuery>
<ErrorMessage><![CDATA[Expression with NodeType 'New' is not supported.]]></ErrorMessage>
<SqlQuery><![CDATA[
SELECT r0["id"] AS keyAlias, COUNT(1) AS count
FROM (
SELECT VALUE root
FROM root
OFFSET 10 LIMIT 10) AS r0
GROUP BY r0["id"]
]]></SqlQuery>
<ErrorMessage><![CDATA[Status Code: BadRequest,{"errors":[{"severity":"Error","location":{"start":81,"end":99},"code":"SC2204","message":"'OFFSET LIMIT' clause is not supported in subqueries."}]},0x800A0B00]]></ErrorMessage>
</Output>
</Result>
<Result>
@ -137,8 +180,11 @@ GROUP BY root ]]></SqlQuery>
<Expression><![CDATA[query.Where(x => (x.Id != "a")).GroupBy(k => k.Id, (key, values) => new AnonymousType(keyAlias = key, count = values.Count()))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[]]></SqlQuery>
<ErrorMessage><![CDATA[Expression with NodeType 'New' is not supported.]]></ErrorMessage>
<SqlQuery><![CDATA[
SELECT root["id"] AS keyAlias, COUNT(1) AS count
FROM root
WHERE (root["id"] != "a")
GROUP BY root["id"] ]]></SqlQuery>
</Output>
</Result>
<Result>
@ -147,8 +193,12 @@ GROUP BY root ]]></SqlQuery>
<Expression><![CDATA[query.OrderBy(x => x.Int).GroupBy(k => k.Id, (key, values) => new AnonymousType(keyAlias = key, count = values.Count()))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[]]></SqlQuery>
<ErrorMessage><![CDATA[Expression with NodeType 'New' is not supported.]]></ErrorMessage>
<SqlQuery><![CDATA[
SELECT root["id"] AS keyAlias, COUNT(1) AS count
FROM root
GROUP BY root["id"]
ORDER BY root["Int"] ASC]]></SqlQuery>
<ErrorMessage><![CDATA[Status Code: BadRequest,{"errors":[{"severity":"Error","location":{"start":89,"end":100},"code":"SC2103","message":"Property reference 'root[\"Int\"]' is invalid in the ORDER BY clause because it is not contained in either an aggregate function or the GROUP BY clause."}]},0x800A0B00]]></ErrorMessage>
</Output>
</Result>
<Result>
@ -157,8 +207,11 @@ GROUP BY root ]]></SqlQuery>
<Expression><![CDATA[query.OrderByDescending(x => x.Id).GroupBy(k => k.Id, (key, values) => new AnonymousType(keyAlias = key, count = values.Count()))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[]]></SqlQuery>
<ErrorMessage><![CDATA[Expression with NodeType 'New' is not supported.]]></ErrorMessage>
<SqlQuery><![CDATA[
SELECT root["id"] AS keyAlias, COUNT(1) AS count
FROM root
GROUP BY root["id"]
ORDER BY root["id"] DESC]]></SqlQuery>
</Output>
</Result>
<Result>
@ -167,8 +220,12 @@ GROUP BY root ]]></SqlQuery>
<Expression><![CDATA[query.Where(x => (x.Id != "a")).OrderBy(x => x.Id).GroupBy(k => k.Id, (key, values) => new AnonymousType(keyAlias = key, count = values.Count()))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[]]></SqlQuery>
<ErrorMessage><![CDATA[Expression with NodeType 'New' is not supported.]]></ErrorMessage>
<SqlQuery><![CDATA[
SELECT root["id"] AS keyAlias, COUNT(1) AS count
FROM root
WHERE (root["id"] != "a")
GROUP BY root["id"]
ORDER BY root["id"] ASC]]></SqlQuery>
</Output>
</Result>
<Result>
@ -178,7 +235,7 @@ GROUP BY root ]]></SqlQuery>
</Input>
<Output>
<SqlQuery><![CDATA[]]></SqlQuery>
<ErrorMessage><![CDATA[Expression with NodeType 'New' is not supported.]]></ErrorMessage>
<ErrorMessage><![CDATA[Group By cannot be followed by other methods]]></ErrorMessage>
</Output>
</Result>
<Result>
@ -188,7 +245,7 @@ GROUP BY root ]]></SqlQuery>
</Input>
<Output>
<SqlQuery><![CDATA[]]></SqlQuery>
<ErrorMessage><![CDATA[Expression with NodeType 'New' is not supported.]]></ErrorMessage>
<ErrorMessage><![CDATA[Group By cannot be followed by other methods]]></ErrorMessage>
</Output>
</Result>
<Result>
@ -198,7 +255,7 @@ GROUP BY root ]]></SqlQuery>
</Input>
<Output>
<SqlQuery><![CDATA[]]></SqlQuery>
<ErrorMessage><![CDATA[Expression with NodeType 'New' is not supported.]]></ErrorMessage>
<ErrorMessage><![CDATA[Group By cannot be followed by other methods]]></ErrorMessage>
</Output>
</Result>
<Result>
@ -208,7 +265,7 @@ GROUP BY root ]]></SqlQuery>
</Input>
<Output>
<SqlQuery><![CDATA[]]></SqlQuery>
<ErrorMessage><![CDATA[Expression with NodeType 'New' is not supported.]]></ErrorMessage>
<ErrorMessage><![CDATA[Group By cannot be followed by other methods]]></ErrorMessage>
</Output>
</Result>
<Result>
@ -218,7 +275,7 @@ GROUP BY root ]]></SqlQuery>
</Input>
<Output>
<SqlQuery><![CDATA[]]></SqlQuery>
<ErrorMessage><![CDATA[Expression with NodeType 'New' is not supported.]]></ErrorMessage>
<ErrorMessage><![CDATA[Group By cannot be followed by other methods]]></ErrorMessage>
</Output>
</Result>
<Result>
@ -228,7 +285,7 @@ GROUP BY root ]]></SqlQuery>
</Input>
<Output>
<SqlQuery><![CDATA[]]></SqlQuery>
<ErrorMessage><![CDATA[Expression with NodeType 'New' is not supported.]]></ErrorMessage>
<ErrorMessage><![CDATA[Group By cannot be followed by other methods]]></ErrorMessage>
</Output>
</Result>
<Result>
@ -238,7 +295,7 @@ GROUP BY root ]]></SqlQuery>
</Input>
<Output>
<SqlQuery><![CDATA[]]></SqlQuery>
<ErrorMessage><![CDATA[Expression with NodeType 'New' is not supported.]]></ErrorMessage>
<ErrorMessage><![CDATA[Group By cannot be followed by other methods]]></ErrorMessage>
</Output>
</Result>
<Result>
@ -248,7 +305,7 @@ GROUP BY root ]]></SqlQuery>
</Input>
<Output>
<SqlQuery><![CDATA[]]></SqlQuery>
<ErrorMessage><![CDATA[Expression with NodeType 'New' is not supported.]]></ErrorMessage>
<ErrorMessage><![CDATA[Group By cannot be followed by other methods]]></ErrorMessage>
</Output>
</Result>
<Result>
@ -258,7 +315,7 @@ GROUP BY root ]]></SqlQuery>
</Input>
<Output>
<SqlQuery><![CDATA[]]></SqlQuery>
<ErrorMessage><![CDATA[Expression with NodeType 'New' is not supported.]]></ErrorMessage>
<ErrorMessage><![CDATA[Group By cannot be followed by other methods]]></ErrorMessage>
</Output>
</Result>
<Result>
@ -268,7 +325,7 @@ GROUP BY root ]]></SqlQuery>
</Input>
<Output>
<SqlQuery><![CDATA[]]></SqlQuery>
<ErrorMessage><![CDATA[Expression with NodeType 'New' is not supported.]]></ErrorMessage>
<ErrorMessage><![CDATA[Group By cannot be followed by other methods]]></ErrorMessage>
</Output>
</Result>
<Result>
@ -278,7 +335,7 @@ GROUP BY root ]]></SqlQuery>
</Input>
<Output>
<SqlQuery><![CDATA[]]></SqlQuery>
<ErrorMessage><![CDATA[Expression with NodeType 'New' is not supported.]]></ErrorMessage>
<ErrorMessage><![CDATA[Group By cannot be followed by other methods]]></ErrorMessage>
</Output>
</Result>
</Results>

Просмотреть файл

@ -167,8 +167,10 @@ GROUP BY root["Int"] ]]></SqlQuery>
<Expression><![CDATA[query.GroupBy(k => k.FamilyId, (key, values) => new AnonymousType(familyId = key, familyIdCount = values.Count()))]]></Expression>
</Input>
<Output>
<SqlQuery><![CDATA[]]></SqlQuery>
<ErrorMessage><![CDATA[Expression with NodeType 'New' is not supported.]]></ErrorMessage>
<SqlQuery><![CDATA[
SELECT root["FamilyId"] AS familyId, COUNT(1) AS familyIdCount
FROM root
GROUP BY root["FamilyId"] ]]></SqlQuery>
</Output>
</Result>
<Result>

Просмотреть файл

@ -747,7 +747,8 @@ namespace Microsoft.Azure.Cosmos.Services.Management.Tests.LinqProviderTests
// Currently unsupported case
inputs.Add(new LinqTestInput("GroupBy Single Value With Min", b => getQuery(b).GroupBy(k => k.FamilyId /*keySelector*/,
(key, values) => new { familyId = key, familyIdCount = values.Count() } /*multi-value select */)));
(key, values) => new { familyId = key, familyIdCount = values.Count() } /*multi-value select */),
ignoreOrderingForAnonymousTypeObject: true));
// Other methods followed by GroupBy
@ -851,48 +852,49 @@ namespace Microsoft.Azure.Cosmos.Services.Management.Tests.LinqProviderTests
{
List<LinqTestInput> inputs = new List<LinqTestInput>();
inputs.Add(new LinqTestInput("GroupBy Multi Value Select Constant", b => getQuery(b).GroupBy(k => k /*keySelector*/,
(key, values) =>
(key, values) =>
new {
stringField = "abv",
numField = 123
})));
numField = 123}),
ignoreOrderingForAnonymousTypeObject: true));
inputs.Add(new LinqTestInput("GroupBy Multi Value Select Key", b => getQuery(b).GroupBy(k => k.Id /*keySelector*/,
(key, values) => new {
Key = key,
key
})));
key}),
ignoreOrderingForAnonymousTypeObject: true));
inputs.Add(new LinqTestInput("GroupBy Multi Value Select Key and Constant", b => getQuery(b).GroupBy(k => k.Id /*keySelector*/,
(key, values) => new {
KeyAlias = key,
values = 123 /* intentionally have the same spelling as the IGrouping values */
})));
values = 123 /* intentionally have the same spelling as the IGrouping values */}),
ignoreOrderingForAnonymousTypeObject: true));
inputs.Add(new LinqTestInput("GroupBy Multi Value With Aggregate", b => getQuery(b).GroupBy(k => k.Id /*keySelector*/,
(key, values) => new {
Min = values.Min(value => value.Int),
Max = values.Max(value => value.Int),
Avg = values.Average(value => value.Int),
Count = values.Count()
})));
Count = values.Count()}),
ignoreOrderingForAnonymousTypeObject: true));
inputs.Add(new LinqTestInput("GroupBy Multi Value With Property Ref and Aggregate", b => getQuery(b).GroupBy(k => k.FamilyId /*keySelector*/,
(key, values) => new {
familyId = key,
familyIdCount = values.Count()
})));
familyIdCount = values.Count()}),
ignoreOrderingForAnonymousTypeObject: true));
// Negative cases
// The translation is correct (SELECT VALUE MIN(root) FROM root GROUP BY root["Number"]
// but the behavior between LINQ and SQL is different
// In Linq, it requires the object to have comparer traits, where as in CosmosDB, we will return null
// In Linq, the queries will return an object on root, where as in CosmosDB, we will return null
inputs.Add(new LinqTestInput("GroupBy Multi Value With Aggregate On Root", b => getQuery(b).GroupBy(k => k.Id /*keySelector*/,
(key, values) => new {
Min = values.Min(),
Max = values.Max()
})));
Max = values.Max()}),
ignoreOrderingForAnonymousTypeObject: true,
skipVerification: true));
// Non-aggregate method calls
inputs.Add(new LinqTestInput("GroupBy Multi Value With Non-Aggregate", b => getQuery(b).GroupBy(k => k.Id /*keySelector*/,
@ -906,50 +908,50 @@ namespace Microsoft.Azure.Cosmos.Services.Management.Tests.LinqProviderTests
.Select(x => x.Id)
.GroupBy(k => k /*keySelector*/, (key, values) => new {
keyAlias = key,
count = values.Count()
})));
count = values.Count()}),
ignoreOrderingForAnonymousTypeObject: true));
inputs.Add(new LinqTestInput("Select + GroupBy 2", b => getQuery(b)
.Select(x => new { Id1 = x.Id, family1 = x.FamilyId, childrenN1 = x.Children })
.GroupBy(k => k.Id1 /*keySelector*/, (key, values) => new {
keyAlias = key,
count = values.Count()
})));
count = values.Count()}),
ignoreOrderingForAnonymousTypeObject: true));
inputs.Add(new LinqTestInput("SelectMany + GroupBy", b => getQuery(b)
.SelectMany(x => x.Children)
.GroupBy(k => k /*keySelector*/, (key, values) => new {
keyAlias = key,
count = values.Count()
})));
count = values.Count()}),
ignoreOrderingForAnonymousTypeObject: true));
inputs.Add(new LinqTestInput("Skip + GroupBy", b => getQuery(b)
.Skip(10)
.GroupBy(k => k.Id /*keySelector*/, (key, values) => new {
keyAlias = key,
count = values.Count()
})));
count = values.Count()}),
ignoreOrderingForAnonymousTypeObject: true));
inputs.Add(new LinqTestInput("Take + GroupBy", b => getQuery(b)
.Take(10)
.GroupBy(k => k.Id /*keySelector*/, (key, values) => new {
keyAlias = key,
count = values.Count()
})));
count = values.Count()}),
ignoreOrderingForAnonymousTypeObject: true));
inputs.Add(new LinqTestInput("Skip + Take + GroupBy", b => getQuery(b)
.Skip(10).Take(10)
.GroupBy(k => k.Id /*keySelector*/, (key, values) => new {
keyAlias = key,
count = values.Count()
})));
count = values.Count()}),
ignoreOrderingForAnonymousTypeObject: true));
inputs.Add(new LinqTestInput("Filter + GroupBy", b => getQuery(b)
.Where(x => x.Id != "a")
.GroupBy(k => k.Id /*keySelector*/, (key, values) => new {
keyAlias = key,
count = values.Count()
})));
count = values.Count()}),
ignoreOrderingForAnonymousTypeObject: true));
// should this become a subquery with order by then group by?
inputs.Add(new LinqTestInput("OrderBy + GroupBy", b => getQuery(b)
@ -979,7 +981,8 @@ namespace Microsoft.Azure.Cosmos.Services.Management.Tests.LinqProviderTests
.GroupBy(k => k.Id /*keySelector*/, (key, values) => new {
keyAlias = key,
count = values.Count()})
.Select(x => x)));
.Select(x => x),
ignoreOrderingForAnonymousTypeObject: true));
inputs.Add(new LinqTestInput("GroupBy + Select 2", b => getQuery(b)
.GroupBy(k => k.Id /*keySelector*/, (key, values) => new {
@ -992,25 +995,29 @@ namespace Microsoft.Azure.Cosmos.Services.Management.Tests.LinqProviderTests
.GroupBy(k => k.Id /*keySelector*/, (key, values) => new {
keyAlias = key,
count = values.Count()})
.Skip(10)));
.Skip(10),
ignoreOrderingForAnonymousTypeObject: true));
inputs.Add(new LinqTestInput("GroupBy + Take", b => getQuery(b)
.GroupBy(k => k.Id /*keySelector*/, (key, values) => new {
keyAlias = key,
count = values.Count()})
.Take(10)));
.Take(10),
ignoreOrderingForAnonymousTypeObject: true));
inputs.Add(new LinqTestInput("GroupBy + Skip + Take", b => getQuery(b)
.GroupBy(k => k.Id /*keySelector*/, (key, values) => new {
keyAlias = key,
count = values.Count()})
.Skip(10).Take(10)));
.Skip(10).Take(10),
ignoreOrderingForAnonymousTypeObject: true));
inputs.Add(new LinqTestInput("GroupBy + Filter", b => getQuery(b)
.GroupBy(k => k.Id /*keySelector*/, (key, values) => new {
keyAlias = key,
count = values.Count()})
.Where(x => x.keyAlias == "a")));
.Where(x => x.keyAlias == "a"),
ignoreOrderingForAnonymousTypeObject: true));
inputs.Add(new LinqTestInput("GroupBy + OrderBy", b => getQuery(b)
.GroupBy(k => k.Id /*keySelector*/, (key, values) => new {
@ -1028,13 +1035,15 @@ namespace Microsoft.Azure.Cosmos.Services.Management.Tests.LinqProviderTests
.GroupBy(k => k.Id /*keySelector*/, (key, values) => new {
keyAlias = key,
count = values.Count()})
.Where(x => x.keyAlias == "a").Skip(10).Take(10)));
.Where(x => x.keyAlias == "a").Skip(10).Take(10),
ignoreOrderingForAnonymousTypeObject: true));
inputs.Add(new LinqTestInput("GroupBy + GroupBy", b => getQuery(b)
.GroupBy(k => k.Id /*keySelector*/, (key, values) => new {
keyAlias = key,
count = values.Count()})
.GroupBy(k => k.count /*keySelector*/, (key, values) => key /*return the group by key */)));
.GroupBy(k => k.count /*keySelector*/, (key, values) => key /*return the group by key */),
ignoreOrderingForAnonymousTypeObject: true));
inputs.Add(new LinqTestInput("GroupBy + GroupBy2", b => getQuery(b)
.GroupBy(k => k.Id /*keySelector*/, (key, values) => new {
@ -1044,7 +1053,8 @@ namespace Microsoft.Azure.Cosmos.Services.Management.Tests.LinqProviderTests
.GroupBy(k => k.count /*keySelector*/, (key, values) => new {
keyAlias = key,
stringField = "abc"
})));
}),
ignoreOrderingForAnonymousTypeObject: true));
this.ExecuteTestSuite(inputs);
}

Просмотреть файл

@ -35,9 +35,38 @@ namespace Microsoft.Azure.Cosmos.Services.Management.Tests
/// <param name="queryResults"></param>
/// <param name="dataResults"></param>
/// <returns></returns>
private static bool CompareListOfAnonymousType(List<object> queryResults, List<dynamic> dataResults)
{
return queryResults.SequenceEqual(dataResults);
private static bool CompareListOfAnonymousType(List<object> queryResults, List<dynamic> dataResults, bool ignoreOrder)
{
if (!ignoreOrder)
{
return queryResults.SequenceEqual(dataResults);
}
if (queryResults.Count != dataResults.Count)
{
return false;
}
bool resultMatched = true;
foreach (object obj in queryResults)
{
if (!dataResults.Any(a => a.Equals(obj)))
{
resultMatched = false;
return false;
}
}
foreach (dynamic obj in dataResults)
{
if (!queryResults.Any(a => a.Equals(obj)))
{
resultMatched = false;
break;
}
}
return resultMatched;
}
/// <summary>
@ -186,7 +215,7 @@ namespace Microsoft.Azure.Cosmos.Services.Management.Tests
/// </summary>
/// <param name="queryResultsList"></param>
/// <param name="dataResultsList"></param>
private static void ValidateResults(List<object> queryResultsList, List<dynamic> dataResultsList)
private static void ValidateResults(List<object> queryResultsList, List<dynamic> dataResultsList, bool ignoreOrder)
{
bool resultMatched = true;
string actualStr = null;
@ -204,7 +233,7 @@ namespace Microsoft.Azure.Cosmos.Services.Management.Tests
}
else if (LinqTestsCommon.IsAnonymousType(firstElem.GetType()))
{
resultMatched &= CompareListOfAnonymousType(queryResultsList, dataResultsList);
resultMatched &= CompareListOfAnonymousType(queryResultsList, dataResultsList, ignoreOrder);
}
else if (LinqTestsCommon.IsNumber(firstElem))
{
@ -548,7 +577,7 @@ namespace Microsoft.Azure.Cosmos.Services.Management.Tests
// we skip unordered query because the LINQ results vs actual query results are non-deterministic
if (!input.skipVerification)
{
LinqTestsCommon.ValidateResults(queryResults, dataResults);
LinqTestsCommon.ValidateResults(queryResults, dataResults, input.ignoreOrder);
}
string serializedResults = serializeResultsInBaseline ?
@ -646,17 +675,22 @@ namespace Microsoft.Azure.Cosmos.Services.Management.Tests
// - unordered query since the results are not deterministics for LinQ results and actual query results
// - scenarios not supported in LINQ, e.g. sequence doesn't contain element.
internal bool skipVerification;
// Ignore Ordering for AnonymousType object
internal readonly bool ignoreOrder;
internal LinqTestInput(
string description,
Expression<Func<bool, IQueryable>> expr,
bool skipVerification = false,
bool skipVerification = false,
bool ignoreOrderingForAnonymousTypeObject = false,
string expressionStr = null,
string inputData = null)
: base(description)
{
this.Expression = expr ?? throw new ArgumentNullException($"{nameof(expr)} must not be null.");
this.skipVerification = skipVerification;
this.skipVerification = skipVerification;
this.ignoreOrder = ignoreOrderingForAnonymousTypeObject;
this.expressionStr = expressionStr;
this.inputData = inputData;
}