Query: Adds translation support for single key single value select GROUP BY LINQ queries (#4074)
* preliminary change * Add some more boiler plate code * move all linq test to the same folder; add some groupBy test * fix references error in test refactoring add code for group by substitution. Still need to adjust binding post groupby * preliminary for the groupby functions with key and value selector * trying to change collection inputs for group by * WIP bookmark * Successfully ignore "key" * clean up code * Sucessfully bind the case of group by with only key selector and no value selector followed by an optional select clause * enable one group by test * add support for aggregate value selector * added baseline * working on adding support for multivalue value selector and key selector * code clean up * more clean up * more clean up * update test * Move test to separate file * code clean up * remove baseline file that got moved * fix merge issue * Changes test infrastructure to reflect changes from Master * address code review part 1 * Address code review 2 and adds code coverage * Addressed code review and added tests. Still a couple of bugs to iron out * Fix group by translation issue and add more test * update comments * address pr comment --------- Co-authored-by: Minh Le <leminh@microsoft.com> Co-authored-by: Aditya <adityasa@users.noreply.github.com>
This commit is contained in:
Родитель
578832669c
Коммит
15d83a7d08
|
@ -8,12 +8,15 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Data.Common;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using Microsoft.Azure.Cosmos.CosmosElements;
|
using Microsoft.Azure.Cosmos.CosmosElements;
|
||||||
|
using Microsoft.Azure.Cosmos.Serialization.HybridRow;
|
||||||
using Microsoft.Azure.Cosmos.Serializer;
|
using Microsoft.Azure.Cosmos.Serializer;
|
||||||
using Microsoft.Azure.Cosmos.Spatial;
|
using Microsoft.Azure.Cosmos.Spatial;
|
||||||
using Microsoft.Azure.Cosmos.SqlObjects;
|
using Microsoft.Azure.Cosmos.SqlObjects;
|
||||||
|
@ -64,6 +67,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
public const string FirstOrDefault = "FirstOrDefault";
|
public const string FirstOrDefault = "FirstOrDefault";
|
||||||
public const string Max = "Max";
|
public const string Max = "Max";
|
||||||
public const string Min = "Min";
|
public const string Min = "Min";
|
||||||
|
public const string GroupBy = "GroupBy";
|
||||||
public const string OrderBy = "OrderBy";
|
public const string OrderBy = "OrderBy";
|
||||||
public const string OrderByDescending = "OrderByDescending";
|
public const string OrderByDescending = "OrderByDescending";
|
||||||
public const string Select = "Select";
|
public const string Select = "Select";
|
||||||
|
@ -109,7 +113,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Translate an expression into a query.
|
/// Translate an expression into a query.
|
||||||
/// Query is constructed as a side-effect in context.currentQuery.
|
/// Query is constructed as a side-effect in context.CurrentQuery.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="inputExpression">Expression to translate.</param>
|
/// <param name="inputExpression">Expression to translate.</param>
|
||||||
/// <param name="context">Context for translation.</param>
|
/// <param name="context">Context for translation.</param>
|
||||||
|
@ -805,8 +809,8 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
|
|
||||||
if (usePropertyRef)
|
if (usePropertyRef)
|
||||||
{
|
{
|
||||||
SqlIdentifier propertyIdnetifier = SqlIdentifier.Create(memberName);
|
SqlIdentifier propertyIdentifier = SqlIdentifier.Create(memberName);
|
||||||
SqlPropertyRefScalarExpression propertyRefExpression = SqlPropertyRefScalarExpression.Create(memberExpression, propertyIdnetifier);
|
SqlPropertyRefScalarExpression propertyRefExpression = SqlPropertyRefScalarExpression.Create(memberExpression, propertyIdentifier);
|
||||||
return propertyRefExpression;
|
return propertyRefExpression;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -997,7 +1001,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
SqlQuery query = context.CurrentQuery.FlattenAsPossible().GetSqlQuery();
|
SqlQuery query = context.CurrentQuery.FlattenAsPossible().GetSqlQuery();
|
||||||
SqlCollection subqueryCollection = SqlSubqueryCollection.Create(query);
|
SqlCollection subqueryCollection = SqlSubqueryCollection.Create(query);
|
||||||
|
|
||||||
ParameterExpression parameterExpression = context.GenFreshParameter(typeof(object), ExpressionToSql.DefaultParameterName);
|
ParameterExpression parameterExpression = context.GenerateFreshParameter(typeof(object), ExpressionToSql.DefaultParameterName);
|
||||||
Binding binding = new Binding(parameterExpression, subqueryCollection, isInCollection: false, isInputParameter: true);
|
Binding binding = new Binding(parameterExpression, subqueryCollection, isInCollection: false, isInputParameter: true);
|
||||||
|
|
||||||
context.CurrentQuery = new QueryUnderConstruction(context.GetGenFreshParameterFunc());
|
context.CurrentQuery = new QueryUnderConstruction(context.GetGenFreshParameterFunc());
|
||||||
|
@ -1111,7 +1115,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
|
|
||||||
Collection collection = ExpressionToSql.ConvertToCollection(body);
|
Collection collection = ExpressionToSql.ConvertToCollection(body);
|
||||||
context.PushCollection(collection);
|
context.PushCollection(collection);
|
||||||
ParameterExpression parameter = context.GenFreshParameter(type, parameterName);
|
ParameterExpression parameter = context.GenerateFreshParameter(type, parameterName);
|
||||||
context.PushParameter(parameter, context.CurrentSubqueryBinding.ShouldBeOnNewQuery);
|
context.PushParameter(parameter, context.CurrentSubqueryBinding.ShouldBeOnNewQuery);
|
||||||
context.PopParameter();
|
context.PopParameter();
|
||||||
context.PopCollection();
|
context.PopCollection();
|
||||||
|
@ -1120,7 +1124,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Visit a method call, construct the corresponding query in context.currentQuery.
|
/// Visit a method call, construct the corresponding query in context.CurrentQuery.
|
||||||
/// At ExpressionToSql point only LINQ method calls are allowed.
|
/// At ExpressionToSql point only LINQ method calls are allowed.
|
||||||
/// These methods are static extension methods of IQueryable or IEnumerable.
|
/// These methods are static extension methods of IQueryable or IEnumerable.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -1149,11 +1153,18 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
|
|
||||||
Type inputElementType = TypeSystem.GetElementType(inputCollection.Type);
|
Type inputElementType = TypeSystem.GetElementType(inputCollection.Type);
|
||||||
Collection collection = ExpressionToSql.Translate(inputCollection, context);
|
Collection collection = ExpressionToSql.Translate(inputCollection, context);
|
||||||
|
|
||||||
context.PushCollection(collection);
|
context.PushCollection(collection);
|
||||||
|
|
||||||
Collection result = new Collection(inputExpression.Method.Name);
|
Collection result = new Collection(inputExpression.Method.Name);
|
||||||
bool shouldBeOnNewQuery = context.CurrentQuery.ShouldBeOnNewQuery(inputExpression.Method.Name, inputExpression.Arguments.Count);
|
bool shouldBeOnNewQuery = context.CurrentQuery.ShouldBeOnNewQuery(inputExpression.Method.Name, inputExpression.Arguments.Count);
|
||||||
context.PushSubqueryBinding(shouldBeOnNewQuery);
|
context.PushSubqueryBinding(shouldBeOnNewQuery);
|
||||||
|
|
||||||
|
if (context.LastExpressionIsGroupBy)
|
||||||
|
{
|
||||||
|
throw new DocumentQueryException(string.Format(CultureInfo.CurrentCulture, "Group By cannot be followed by other methods"));
|
||||||
|
}
|
||||||
|
|
||||||
switch (inputExpression.Method.Name)
|
switch (inputExpression.Method.Name)
|
||||||
{
|
{
|
||||||
case LinqMethods.Any:
|
case LinqMethods.Any:
|
||||||
|
@ -1219,6 +1230,13 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
context.CurrentQuery = context.CurrentQuery.AddSelectClause(select, context);
|
context.CurrentQuery = context.CurrentQuery.AddSelectClause(select, context);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case LinqMethods.GroupBy:
|
||||||
|
{
|
||||||
|
context.CurrentQuery = context.PackageCurrentQueryIfNeccessary();
|
||||||
|
result = ExpressionToSql.VisitGroupBy(returnElementType, inputExpression.Arguments, context);
|
||||||
|
context.LastExpressionIsGroupBy = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
case LinqMethods.OrderBy:
|
case LinqMethods.OrderBy:
|
||||||
{
|
{
|
||||||
SqlOrderByClause orderBy = ExpressionToSql.VisitOrderBy(inputExpression.Arguments, false, context);
|
SqlOrderByClause orderBy = ExpressionToSql.VisitOrderBy(inputExpression.Arguments, false, context);
|
||||||
|
@ -1376,6 +1394,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
case LinqMethods.Skip:
|
case LinqMethods.Skip:
|
||||||
case LinqMethods.Take:
|
case LinqMethods.Take:
|
||||||
case LinqMethods.Distinct:
|
case LinqMethods.Distinct:
|
||||||
|
case LinqMethods.GroupBy:
|
||||||
isSubqueryExpression = true;
|
isSubqueryExpression = true;
|
||||||
expressionObjKind = SubqueryKind.ArrayScalarExpression;
|
expressionObjKind = SubqueryKind.ArrayScalarExpression;
|
||||||
break;
|
break;
|
||||||
|
@ -1405,7 +1424,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Visit an lambda expression which is in side a lambda and translate it to a scalar expression or a collection scalar expression.
|
/// Visit an lambda expression which is inside a lambda and translate it to a scalar expression or a collection scalar expression.
|
||||||
/// If it is a collection scalar expression, e.g. should be translated to subquery such as SELECT VALUE ARRAY, SELECT VALUE EXISTS,
|
/// If it is a collection scalar expression, e.g. should be translated to subquery such as SELECT VALUE ARRAY, SELECT VALUE EXISTS,
|
||||||
/// SELECT VALUE [aggregate], the subquery will be aliased to a new binding for the FROM clause. E.g. consider
|
/// SELECT VALUE [aggregate], the subquery will be aliased to a new binding for the FROM clause. E.g. consider
|
||||||
/// Select(family => family.Children.Select(child => child.Grade)). Since the inner Select corresponds to a subquery, this method would
|
/// Select(family => family.Children.Select(child => child.Grade)). Since the inner Select corresponds to a subquery, this method would
|
||||||
|
@ -1508,7 +1527,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
{
|
{
|
||||||
SqlQuery query = ExpressionToSql.CreateSubquery(expression, parameters, context);
|
SqlQuery query = ExpressionToSql.CreateSubquery(expression, parameters, context);
|
||||||
|
|
||||||
ParameterExpression parameterExpression = context.GenFreshParameter(typeof(object), ExpressionToSql.DefaultParameterName);
|
ParameterExpression parameterExpression = context.GenerateFreshParameter(typeof(object), ExpressionToSql.DefaultParameterName);
|
||||||
SqlCollection subqueryCollection = ExpressionToSql.CreateSubquerySqlCollection(
|
SqlCollection subqueryCollection = ExpressionToSql.CreateSubquerySqlCollection(
|
||||||
query,
|
query,
|
||||||
isMinMaxAvgMethod ? SubqueryKind.ArrayScalarExpression : expressionObjKind.Value);
|
isMinMaxAvgMethod ? SubqueryKind.ArrayScalarExpression : expressionObjKind.Value);
|
||||||
|
@ -1585,7 +1604,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
|
|
||||||
QueryUnderConstruction queryBeforeVisit = context.CurrentQuery;
|
QueryUnderConstruction queryBeforeVisit = context.CurrentQuery;
|
||||||
QueryUnderConstruction packagedQuery = new QueryUnderConstruction(context.GetGenFreshParameterFunc(), context.CurrentQuery);
|
QueryUnderConstruction packagedQuery = new QueryUnderConstruction(context.GetGenFreshParameterFunc(), context.CurrentQuery);
|
||||||
packagedQuery.fromParameters.SetInputParameter(typeof(object), context.CurrentQuery.GetInputParameterInContext(shouldBeOnNewQuery).Name, context.InScope);
|
packagedQuery.FromParameters.SetInputParameter(typeof(object), context.CurrentQuery.GetInputParameterInContext(shouldBeOnNewQuery).Name, context.InScope);
|
||||||
context.CurrentQuery = packagedQuery;
|
context.CurrentQuery = packagedQuery;
|
||||||
|
|
||||||
if (shouldBeOnNewQuery) context.CurrentSubqueryBinding.ShouldBeOnNewQuery = false;
|
if (shouldBeOnNewQuery) context.CurrentSubqueryBinding.ShouldBeOnNewQuery = false;
|
||||||
|
@ -1663,9 +1682,108 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
Binding binding;
|
Binding binding;
|
||||||
SqlQuery query = ExpressionToSql.CreateSubquery(lambda.Body, lambda.Parameters, context);
|
SqlQuery query = ExpressionToSql.CreateSubquery(lambda.Body, lambda.Parameters, context);
|
||||||
SqlCollection subqueryCollection = SqlSubqueryCollection.Create(query);
|
SqlCollection subqueryCollection = SqlSubqueryCollection.Create(query);
|
||||||
ParameterExpression parameterExpression = context.GenFreshParameter(typeof(object), ExpressionToSql.DefaultParameterName);
|
ParameterExpression parameterExpression = context.GenerateFreshParameter(typeof(object), ExpressionToSql.DefaultParameterName);
|
||||||
binding = new Binding(parameterExpression, subqueryCollection, isInCollection: false, isInputParameter: true);
|
binding = new Binding(parameterExpression, subqueryCollection, isInCollection: false, isInputParameter: true);
|
||||||
context.CurrentQuery.fromParameters.Add(binding);
|
context.CurrentQuery.FromParameters.Add(binding);
|
||||||
|
}
|
||||||
|
|
||||||
|
return collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Collection VisitGroupBy(Type returnElementType, ReadOnlyCollection<Expression> arguments, TranslationContext context)
|
||||||
|
{
|
||||||
|
if (arguments.Count != 3)
|
||||||
|
{
|
||||||
|
throw new DocumentQueryException(string.Format(CultureInfo.CurrentCulture, ClientResources.InvalidArgumentsCount, LinqMethods.GroupBy, 3, arguments.Count));
|
||||||
|
}
|
||||||
|
|
||||||
|
// bind the parameters in the value selector to the current input
|
||||||
|
foreach (ParameterExpression par in Utilities.GetLambda(arguments[2]).Parameters)
|
||||||
|
{
|
||||||
|
context.PushParameter(par, context.CurrentSubqueryBinding.ShouldBeOnNewQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
// First argument is input, second is key selector and third is value selector
|
||||||
|
LambdaExpression keySelectorLambda = Utilities.GetLambda(arguments[1]);
|
||||||
|
|
||||||
|
// Current GroupBy doesn't allow subquery, so we need to visit non subquery scalar lambda
|
||||||
|
SqlScalarExpression keySelectorFunc = ExpressionToSql.VisitNonSubqueryScalarLambda(keySelectorLambda, context);
|
||||||
|
|
||||||
|
SqlGroupByClause groupby = SqlGroupByClause.Create(keySelectorFunc);
|
||||||
|
|
||||||
|
context.CurrentQuery = context.CurrentQuery.AddGroupByClause(groupby, context);
|
||||||
|
|
||||||
|
// Create a GroupBy collection and bind the new GroupBy collection to the new parameters created from the key
|
||||||
|
Collection collection = ExpressionToSql.ConvertToCollection(keySelectorFunc);
|
||||||
|
collection.isOuter = true;
|
||||||
|
collection.Name = "GroupBy";
|
||||||
|
|
||||||
|
ParameterExpression parameterExpression = context.GenerateFreshParameter(returnElementType, keySelectorFunc.ToString(), includeSuffix: false);
|
||||||
|
Binding binding = new Binding(parameterExpression, collection.inner, isInCollection: false, isInputParameter: true);
|
||||||
|
|
||||||
|
context.CurrentQuery.GroupByParameter = new FromParameterBindings();
|
||||||
|
context.CurrentQuery.GroupByParameter.Add(binding);
|
||||||
|
|
||||||
|
// 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
|
||||||
|
ParameterExpression valueSelectorKeyExpressionAlias = Utilities.GetLambda(arguments[2]).Parameters[0];
|
||||||
|
context.GroupByKeySubstitution.AddSubstitution(valueSelectorKeyExpressionAlias, parameterExpression/*Utilities.GetLambda(arguments[1]).Body*/);
|
||||||
|
|
||||||
|
// Translate the body of the value selector lambda
|
||||||
|
Expression valueSelectorExpression = Utilities.GetLambda(arguments[2]).Body;
|
||||||
|
|
||||||
|
// The value selector function needs to be either a MethodCall or an AnonymousType
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ExpressionType.New:
|
||||||
|
// TODO: Multi Value Selector
|
||||||
|
throw new DocumentQueryException(string.Format(CultureInfo.CurrentCulture, ClientResources.ExpressionTypeIsNotSupported, ExpressionType.New));
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new DocumentQueryException(string.Format(CultureInfo.CurrentCulture, ClientResources.ExpressionTypeIsNotSupported, valueSelectorExpression.NodeType));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (ParameterExpression par in Utilities.GetLambda(arguments[2]).Parameters)
|
||||||
|
{
|
||||||
|
context.PopParameter();
|
||||||
}
|
}
|
||||||
|
|
||||||
return collection;
|
return collection;
|
||||||
|
@ -1700,7 +1818,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
// it is necessary to trigger the binding because Skip is just a spec with no binding on its own.
|
// it is necessary to trigger the binding because Skip is just a spec with no binding on its own.
|
||||||
// This can be done by pushing and popping a temporary parameter. E.g. In SelectMany(f => f.Children.Skip(1)),
|
// This can be done by pushing and popping a temporary parameter. E.g. In SelectMany(f => f.Children.Skip(1)),
|
||||||
// it's necessary to consider Skip as Skip(x => x, 1) to bind x to f.Children. Similarly for Top and Limit.
|
// it's necessary to consider Skip as Skip(x => x, 1) to bind x to f.Children. Similarly for Top and Limit.
|
||||||
ParameterExpression parameter = context.GenFreshParameter(typeof(object), ExpressionToSql.DefaultParameterName);
|
ParameterExpression parameter = context.GenerateFreshParameter(typeof(object), ExpressionToSql.DefaultParameterName);
|
||||||
context.PushParameter(parameter, context.CurrentSubqueryBinding.ShouldBeOnNewQuery);
|
context.PushParameter(parameter, context.CurrentSubqueryBinding.ShouldBeOnNewQuery);
|
||||||
context.PopParameter();
|
context.PopParameter();
|
||||||
|
|
||||||
|
@ -1848,16 +1966,21 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
SqlScalarExpression aggregateExpression;
|
SqlScalarExpression aggregateExpression;
|
||||||
if (arguments.Count == 1)
|
if (arguments.Count == 1)
|
||||||
{
|
{
|
||||||
// Need to trigger parameter binding for cases where a aggregate function immediately follows a member access.
|
// Need to trigger parameter binding for cases where an aggregate function immediately follows a member access.
|
||||||
ParameterExpression parameter = context.GenFreshParameter(typeof(object), ExpressionToSql.DefaultParameterName);
|
ParameterExpression parameter = context.GenerateFreshParameter(typeof(object), ExpressionToSql.DefaultParameterName);
|
||||||
context.PushParameter(parameter, context.CurrentSubqueryBinding.ShouldBeOnNewQuery);
|
context.PushParameter(parameter, context.CurrentSubqueryBinding.ShouldBeOnNewQuery);
|
||||||
|
|
||||||
|
// If there is a groupby, since there is no argument to the aggregate, we consider it to be invoked on the source collection, and not the group by keys
|
||||||
aggregateExpression = ExpressionToSql.VisitParameter(parameter, context);
|
aggregateExpression = ExpressionToSql.VisitParameter(parameter, context);
|
||||||
context.PopParameter();
|
context.PopParameter();
|
||||||
}
|
}
|
||||||
else if (arguments.Count == 2)
|
else if (arguments.Count == 2)
|
||||||
{
|
{
|
||||||
LambdaExpression lambda = Utilities.GetLambda(arguments[1]);
|
LambdaExpression lambda = Utilities.GetLambda(arguments[1]);
|
||||||
aggregateExpression = ExpressionToSql.VisitScalarExpression(lambda, context);
|
|
||||||
|
aggregateExpression = context.CurrentQuery.GroupByParameter != null
|
||||||
|
? ExpressionToSql.VisitNonSubqueryScalarLambda(lambda, context)
|
||||||
|
: ExpressionToSql.VisitScalarExpression(lambda, context);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -1884,7 +2007,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
|
|
||||||
// We consider Distinct as Distinct(v0 => v0)
|
// We consider Distinct as Distinct(v0 => v0)
|
||||||
// It's necessary to visit this identity method to replace the parameters names
|
// It's necessary to visit this identity method to replace the parameters names
|
||||||
ParameterExpression parameter = context.GenFreshParameter(typeof(object), ExpressionToSql.DefaultParameterName);
|
ParameterExpression parameter = context.GenerateFreshParameter(typeof(object), ExpressionToSql.DefaultParameterName);
|
||||||
LambdaExpression identityLambda = Expression.Lambda(parameter, parameter);
|
LambdaExpression identityLambda = Expression.Lambda(parameter, parameter);
|
||||||
SqlScalarExpression sqlfunc = ExpressionToSql.VisitNonSubqueryScalarLambda(identityLambda, context);
|
SqlScalarExpression sqlfunc = ExpressionToSql.VisitNonSubqueryScalarLambda(identityLambda, context);
|
||||||
SqlSelectSpec sqlSpec = SqlSelectValueSpec.Create(sqlfunc);
|
SqlSelectSpec sqlSpec = SqlSelectValueSpec.Create(sqlfunc);
|
||||||
|
|
|
@ -27,7 +27,16 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Binding for the FROM parameters.
|
/// Binding for the FROM parameters.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public FromParameterBindings fromParameters
|
public FromParameterBindings FromParameters
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Binding for the Group By clause.
|
||||||
|
/// </summary>
|
||||||
|
public FromParameterBindings GroupByParameter
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
set;
|
set;
|
||||||
|
@ -51,6 +60,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
private SqlSelectClause selectClause;
|
private SqlSelectClause selectClause;
|
||||||
private SqlWhereClause whereClause;
|
private SqlWhereClause whereClause;
|
||||||
private SqlOrderByClause orderByClause;
|
private SqlOrderByClause orderByClause;
|
||||||
|
private SqlGroupByClause groupByClause;
|
||||||
|
|
||||||
// The specs could be in clauses to reflect the SqlQuery.
|
// The specs could be in clauses to reflect the SqlQuery.
|
||||||
// However, they are separated to avoid update recreation of the readonly DOMs and lengthy code.
|
// However, they are separated to avoid update recreation of the readonly DOMs and lengthy code.
|
||||||
|
@ -61,7 +71,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
private Lazy<ParameterExpression> alias;
|
private Lazy<ParameterExpression> alias;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Input subquery.
|
/// Input subquery / query to the left of the current query.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private QueryUnderConstruction inputQuery;
|
private QueryUnderConstruction inputQuery;
|
||||||
|
|
||||||
|
@ -72,7 +82,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
|
|
||||||
public QueryUnderConstruction(Func<string, ParameterExpression> aliasCreatorFunc, QueryUnderConstruction inputQuery)
|
public QueryUnderConstruction(Func<string, ParameterExpression> aliasCreatorFunc, QueryUnderConstruction inputQuery)
|
||||||
{
|
{
|
||||||
this.fromParameters = new FromParameterBindings();
|
this.FromParameters = new FromParameterBindings();
|
||||||
this.aliasCreatorFunc = aliasCreatorFunc;
|
this.aliasCreatorFunc = aliasCreatorFunc;
|
||||||
this.inputQuery = inputQuery;
|
this.inputQuery = inputQuery;
|
||||||
this.alias = new Lazy<ParameterExpression>(() => aliasCreatorFunc(QueryUnderConstruction.DefaultSubqueryRoot));
|
this.alias = new Lazy<ParameterExpression>(() => aliasCreatorFunc(QueryUnderConstruction.DefaultSubqueryRoot));
|
||||||
|
@ -85,22 +95,22 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
|
|
||||||
public void AddBinding(Binding binding)
|
public void AddBinding(Binding binding)
|
||||||
{
|
{
|
||||||
this.fromParameters.Add(binding);
|
this.FromParameters.Add(binding);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ParameterExpression GetInputParameterInContext(bool isInNewQuery)
|
public ParameterExpression GetInputParameterInContext(bool isInNewQuery)
|
||||||
{
|
{
|
||||||
return isInNewQuery ? this.Alias : this.fromParameters.GetInputParameter();
|
return isInNewQuery ? this.Alias : this.FromParameters.GetInputParameter();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a FROM clause from a set of FROM parameter bindings.
|
/// Create a FROM clause from a set of FROM parameter bindings.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The created FROM clause.</returns>
|
/// <returns>The created FROM clause.</returns>
|
||||||
private SqlFromClause CreateFrom(SqlCollectionExpression inputCollectionExpression)
|
private SqlFromClause CreateFromClause(SqlCollectionExpression inputCollectionExpression)
|
||||||
{
|
{
|
||||||
bool first = true;
|
bool first = true;
|
||||||
foreach (Binding paramDef in this.fromParameters.GetBindings())
|
foreach (Binding paramDef in this.FromParameters.GetBindings())
|
||||||
{
|
{
|
||||||
// If input collection expression is provided, the first binding,
|
// If input collection expression is provided, the first binding,
|
||||||
// which is the input paramter name, should be omitted.
|
// which is the input paramter name, should be omitted.
|
||||||
|
@ -147,7 +157,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
ParameterExpression inputParam = this.inputQuery.Alias;
|
ParameterExpression inputParam = this.inputQuery.Alias;
|
||||||
SqlIdentifier identifier = SqlIdentifier.Create(inputParam.Name);
|
SqlIdentifier identifier = SqlIdentifier.Create(inputParam.Name);
|
||||||
SqlAliasedCollectionExpression colExp = SqlAliasedCollectionExpression.Create(collection, identifier);
|
SqlAliasedCollectionExpression colExp = SqlAliasedCollectionExpression.Create(collection, identifier);
|
||||||
SqlFromClause fromClause = this.CreateFrom(colExp);
|
SqlFromClause fromClause = this.CreateFromClause(colExp);
|
||||||
return fromClause;
|
return fromClause;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,7 +179,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
fromClause = this.CreateFrom(inputCollectionExpression: null);
|
fromClause = this.CreateFromClause(inputCollectionExpression: null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a SqlSelectClause with the topSpec.
|
// Create a SqlSelectClause with the topSpec.
|
||||||
|
@ -178,7 +188,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
SqlSelectClause selectClause = this.selectClause;
|
SqlSelectClause selectClause = this.selectClause;
|
||||||
if (selectClause == null)
|
if (selectClause == null)
|
||||||
{
|
{
|
||||||
string parameterName = this.fromParameters.GetInputParameter().Name;
|
string parameterName = this.FromParameters.GetInputParameter().Name;
|
||||||
SqlScalarExpression parameterExpression = SqlPropertyRefScalarExpression.Create(null, SqlIdentifier.Create(parameterName));
|
SqlScalarExpression parameterExpression = SqlPropertyRefScalarExpression.Create(null, SqlIdentifier.Create(parameterName));
|
||||||
selectClause = this.selectClause = SqlSelectClause.Create(SqlSelectValueSpec.Create(parameterExpression));
|
selectClause = this.selectClause = SqlSelectClause.Create(SqlSelectValueSpec.Create(parameterExpression));
|
||||||
}
|
}
|
||||||
|
@ -186,7 +196,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
SqlOffsetLimitClause offsetLimitClause = (this.offsetSpec != null) ?
|
SqlOffsetLimitClause offsetLimitClause = (this.offsetSpec != null) ?
|
||||||
SqlOffsetLimitClause.Create(this.offsetSpec, this.limitSpec ?? SqlLimitSpec.Create(SqlNumberLiteral.Create(int.MaxValue))) :
|
SqlOffsetLimitClause.Create(this.offsetSpec, this.limitSpec ?? SqlLimitSpec.Create(SqlNumberLiteral.Create(int.MaxValue))) :
|
||||||
offsetLimitClause = default(SqlOffsetLimitClause);
|
offsetLimitClause = default(SqlOffsetLimitClause);
|
||||||
SqlQuery result = SqlQuery.Create(selectClause, fromClause, this.whereClause, /*GroupBy*/ null, this.orderByClause, offsetLimitClause);
|
SqlQuery result = SqlQuery.Create(selectClause, fromClause, this.whereClause, this.groupByClause, this.orderByClause, offsetLimitClause);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,7 +208,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
public QueryUnderConstruction PackageQuery(HashSet<ParameterExpression> inScope)
|
public QueryUnderConstruction PackageQuery(HashSet<ParameterExpression> inScope)
|
||||||
{
|
{
|
||||||
QueryUnderConstruction result = new QueryUnderConstruction(this.aliasCreatorFunc);
|
QueryUnderConstruction result = new QueryUnderConstruction(this.aliasCreatorFunc);
|
||||||
result.fromParameters.SetInputParameter(typeof(object), this.Alias.Name, inScope);
|
result.FromParameters.SetInputParameter(typeof(object), this.Alias.Name, inScope);
|
||||||
result.inputQuery = this;
|
result.inputQuery = this;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -214,13 +224,14 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
// 1. Select clause appears after Distinct
|
// 1. Select clause appears after Distinct
|
||||||
// 2. There are any operations after Take that is not a pure Select.
|
// 2. There are any operations after Take that is not a pure Select.
|
||||||
// 3. There are nested Select, Where or OrderBy
|
// 3. There are nested Select, Where or OrderBy
|
||||||
|
// 4. Group by clause appears after Select
|
||||||
QueryUnderConstruction parentQuery = null;
|
QueryUnderConstruction parentQuery = null;
|
||||||
QueryUnderConstruction flattenQuery = null;
|
QueryUnderConstruction flattenQuery = null;
|
||||||
bool seenSelect = false;
|
bool seenSelect = false;
|
||||||
bool seenAnyNonSelectOp = false;
|
bool seenAnyNonSelectOp = false;
|
||||||
for (QueryUnderConstruction query = this; query != null; query = query.inputQuery)
|
for (QueryUnderConstruction query = this; query != null; query = query.inputQuery)
|
||||||
{
|
{
|
||||||
foreach (Binding binding in query.fromParameters.GetBindings())
|
foreach (Binding binding in query.FromParameters.GetBindings())
|
||||||
{
|
{
|
||||||
if ((binding.ParameterDefinition != null) && (binding.ParameterDefinition is SqlSubqueryCollection))
|
if ((binding.ParameterDefinition != null) && (binding.ParameterDefinition is SqlSubqueryCollection))
|
||||||
{
|
{
|
||||||
|
@ -232,8 +243,15 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
// In Select -> SelectMany cases, fromParameter substitution is not yet supported .
|
// In Select -> SelectMany cases, fromParameter substitution is not yet supported .
|
||||||
// Therefore these are un-flattenable.
|
// Therefore these are un-flattenable.
|
||||||
if (query.inputQuery != null &&
|
if (query.inputQuery != null &&
|
||||||
(query.fromParameters.GetBindings().First().Parameter.Name == query.inputQuery.Alias.Name) &&
|
(query.FromParameters.GetBindings().First().Parameter.Name == query.inputQuery.Alias.Name) &&
|
||||||
query.fromParameters.GetBindings().Any(b => b.ParameterDefinition != null))
|
query.FromParameters.GetBindings().Any(b => b.ParameterDefinition != null))
|
||||||
|
{
|
||||||
|
flattenQuery = this;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In case of Select -> Group by cases, the Select query should not be flattened and kept as a subquery
|
||||||
|
if ((query.inputQuery?.selectClause != null) && (query.groupByClause != null))
|
||||||
{
|
{
|
||||||
flattenQuery = this;
|
flattenQuery = this;
|
||||||
break;
|
break;
|
||||||
|
@ -253,10 +271,12 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
seenAnyNonSelectOp |=
|
seenAnyNonSelectOp |=
|
||||||
(query.whereClause != null) ||
|
(query.whereClause != null) ||
|
||||||
(query.orderByClause != null) ||
|
(query.orderByClause != null) ||
|
||||||
|
(query.groupByClause != null) ||
|
||||||
(query.topSpec != null) ||
|
(query.topSpec != null) ||
|
||||||
(query.offsetSpec != null) ||
|
(query.offsetSpec != null) ||
|
||||||
query.fromParameters.GetBindings().Any(b => b.ParameterDefinition != null) ||
|
query.FromParameters.GetBindings().Any(b => b.ParameterDefinition != null) ||
|
||||||
((query.selectClause != null) && (query.selectClause.HasDistinct || this.HasSelectAggregate()));
|
((query.selectClause != null) && (query.selectClause.HasDistinct ||
|
||||||
|
this.HasSelectAggregate()));
|
||||||
parentQuery = query;
|
parentQuery = query;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -272,7 +292,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
private QueryUnderConstruction Flatten()
|
private QueryUnderConstruction Flatten()
|
||||||
{
|
{
|
||||||
// SELECT fo(y) FROM y IN (SELECT fi(x) FROM x WHERE gi(x)) WHERE go(y)
|
// SELECT fo(y) FROM y IN (SELECT fi(x) FROM x WHERE gi(x)) WHERE go(y)
|
||||||
// is translated by substituting fi(x) for y in the outer query
|
// is translated by substituting y for fi(x) in the outer query
|
||||||
// producing
|
// producing
|
||||||
// SELECT fo(fi(x)) FROM x WHERE gi(x) AND (go(fi(x))
|
// SELECT fo(fi(x)) FROM x WHERE gi(x) AND (go(fi(x))
|
||||||
if (this.inputQuery == null)
|
if (this.inputQuery == null)
|
||||||
|
@ -281,7 +301,8 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
if (this.selectClause == null)
|
if (this.selectClause == null)
|
||||||
{
|
{
|
||||||
// If selectClause doesn't exists, use SELECT v0 where v0 is the input parameter, instead of SELECT *.
|
// If selectClause doesn't exists, use SELECT v0 where v0 is the input parameter, instead of SELECT *.
|
||||||
string parameterName = this.fromParameters.GetInputParameter().Name;
|
// If there is a groupby clause, the input parameter comes from the groupBy binding instead of the from clause binding
|
||||||
|
string parameterName = (this.GroupByParameter ?? this.FromParameters).GetInputParameter().Name;
|
||||||
SqlScalarExpression parameterExpression = SqlPropertyRefScalarExpression.Create(null, SqlIdentifier.Create(parameterName));
|
SqlScalarExpression parameterExpression = SqlPropertyRefScalarExpression.Create(null, SqlIdentifier.Create(parameterName));
|
||||||
this.selectClause = SqlSelectClause.Create(SqlSelectValueSpec.Create(parameterExpression));
|
this.selectClause = SqlSelectClause.Create(SqlSelectValueSpec.Create(parameterExpression));
|
||||||
}
|
}
|
||||||
|
@ -302,12 +323,12 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
// That is because if it has been binded before, it has global scope and should not be replaced.
|
// That is because if it has been binded before, it has global scope and should not be replaced.
|
||||||
string paramName = null;
|
string paramName = null;
|
||||||
HashSet<string> inputQueryParams = new HashSet<string>();
|
HashSet<string> inputQueryParams = new HashSet<string>();
|
||||||
foreach (Binding binding in this.inputQuery.fromParameters.GetBindings())
|
foreach (Binding binding in this.inputQuery.FromParameters.GetBindings())
|
||||||
{
|
{
|
||||||
inputQueryParams.Add(binding.Parameter.Name);
|
inputQueryParams.Add(binding.Parameter.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (Binding binding in this.fromParameters.GetBindings())
|
foreach (Binding binding in this.FromParameters.GetBindings())
|
||||||
{
|
{
|
||||||
if (binding.ParameterDefinition == null || inputQueryParams.Contains(binding.Parameter.Name))
|
if (binding.ParameterDefinition == null || inputQueryParams.Contains(binding.Parameter.Name))
|
||||||
{
|
{
|
||||||
|
@ -316,11 +337,14 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
}
|
}
|
||||||
|
|
||||||
SqlIdentifier replacement = SqlIdentifier.Create(paramName);
|
SqlIdentifier replacement = SqlIdentifier.Create(paramName);
|
||||||
SqlSelectClause composedSelect = this.Substitute(inputSelect, inputSelect.TopSpec ?? this.topSpec, replacement, this.selectClause);
|
SqlSelectClause composedSelect;
|
||||||
|
|
||||||
|
composedSelect = this.Substitute(inputSelect, inputSelect.TopSpec ?? this.topSpec, replacement, this.selectClause);
|
||||||
SqlWhereClause composedWhere = this.Substitute(inputSelect.SelectSpec, replacement, this.whereClause);
|
SqlWhereClause composedWhere = this.Substitute(inputSelect.SelectSpec, replacement, this.whereClause);
|
||||||
SqlOrderByClause composedOrderBy = this.Substitute(inputSelect.SelectSpec, replacement, this.orderByClause);
|
SqlOrderByClause composedOrderBy = this.Substitute(inputSelect.SelectSpec, replacement, this.orderByClause);
|
||||||
|
SqlGroupByClause composedGroupBy = this.Substitute(inputSelect.SelectSpec, replacement, this.groupByClause);
|
||||||
SqlWhereClause and = QueryUnderConstruction.CombineWithConjunction(inputwhere, composedWhere);
|
SqlWhereClause and = QueryUnderConstruction.CombineWithConjunction(inputwhere, composedWhere);
|
||||||
FromParameterBindings fromParams = QueryUnderConstruction.CombineInputParameters(flatInput.fromParameters, this.fromParameters);
|
FromParameterBindings fromParams = QueryUnderConstruction.CombineInputParameters(flatInput.FromParameters, this.FromParameters);
|
||||||
SqlOffsetSpec offsetSpec;
|
SqlOffsetSpec offsetSpec;
|
||||||
SqlLimitSpec limitSpec;
|
SqlLimitSpec limitSpec;
|
||||||
if (flatInput.offsetSpec != null)
|
if (flatInput.offsetSpec != null)
|
||||||
|
@ -338,8 +362,9 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
selectClause = composedSelect,
|
selectClause = composedSelect,
|
||||||
whereClause = and,
|
whereClause = and,
|
||||||
inputQuery = null,
|
inputQuery = null,
|
||||||
fromParameters = flatInput.fromParameters,
|
FromParameters = flatInput.FromParameters,
|
||||||
orderByClause = composedOrderBy ?? this.inputQuery.orderByClause,
|
orderByClause = composedOrderBy ?? this.inputQuery.orderByClause,
|
||||||
|
groupByClause = composedGroupBy ?? this.inputQuery.groupByClause,
|
||||||
offsetSpec = offsetSpec,
|
offsetSpec = offsetSpec,
|
||||||
limitSpec = limitSpec,
|
limitSpec = limitSpec,
|
||||||
alias = new Lazy<ParameterExpression>(() => this.Alias)
|
alias = new Lazy<ParameterExpression>(() => this.Alias)
|
||||||
|
@ -349,25 +374,25 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
|
|
||||||
private SqlSelectClause Substitute(SqlSelectClause inputSelectClause, SqlTopSpec topSpec, SqlIdentifier inputParam, SqlSelectClause selectClause)
|
private SqlSelectClause Substitute(SqlSelectClause inputSelectClause, SqlTopSpec topSpec, SqlIdentifier inputParam, SqlSelectClause selectClause)
|
||||||
{
|
{
|
||||||
SqlSelectSpec selectSpec = inputSelectClause.SelectSpec;
|
SqlSelectSpec inputSelectSpec = inputSelectClause.SelectSpec;
|
||||||
|
|
||||||
if (selectClause == null)
|
if (selectClause == null)
|
||||||
{
|
{
|
||||||
return selectSpec != null ? SqlSelectClause.Create(selectSpec, topSpec, inputSelectClause.HasDistinct) : null;
|
return inputSelectSpec != null ? SqlSelectClause.Create(inputSelectSpec, topSpec, inputSelectClause.HasDistinct) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectSpec is SqlSelectStarSpec)
|
if (inputSelectSpec is SqlSelectStarSpec)
|
||||||
{
|
{
|
||||||
return SqlSelectClause.Create(selectSpec, topSpec, inputSelectClause.HasDistinct);
|
return SqlSelectClause.Create(inputSelectSpec, topSpec, inputSelectClause.HasDistinct);
|
||||||
}
|
}
|
||||||
|
|
||||||
SqlSelectValueSpec selValue = selectSpec as SqlSelectValueSpec;
|
SqlSelectValueSpec selValue = inputSelectSpec as SqlSelectValueSpec;
|
||||||
if (selValue != null)
|
if (selValue != null)
|
||||||
{
|
{
|
||||||
SqlSelectSpec intoSpec = selectClause.SelectSpec;
|
SqlSelectSpec intoSpec = selectClause.SelectSpec;
|
||||||
if (intoSpec is SqlSelectStarSpec)
|
if (intoSpec is SqlSelectStarSpec)
|
||||||
{
|
{
|
||||||
return SqlSelectClause.Create(selectSpec, topSpec, selectClause.HasDistinct || inputSelectClause.HasDistinct);
|
return SqlSelectClause.Create(inputSelectSpec, topSpec, selectClause.HasDistinct || inputSelectClause.HasDistinct);
|
||||||
}
|
}
|
||||||
|
|
||||||
SqlSelectValueSpec intoSelValue = intoSpec as SqlSelectValueSpec;
|
SqlSelectValueSpec intoSelValue = intoSpec as SqlSelectValueSpec;
|
||||||
|
@ -381,7 +406,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
throw new DocumentQueryException("Unexpected SQL select clause type: " + intoSpec.GetType());
|
throw new DocumentQueryException("Unexpected SQL select clause type: " + intoSpec.GetType());
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new DocumentQueryException("Unexpected SQL select clause type: " + selectSpec.GetType());
|
throw new DocumentQueryException("Unexpected SQL select clause type: " + inputSelectSpec.GetType());
|
||||||
}
|
}
|
||||||
|
|
||||||
private SqlWhereClause Substitute(SqlSelectSpec spec, SqlIdentifier inputParam, SqlWhereClause whereClause)
|
private SqlWhereClause Substitute(SqlSelectSpec spec, SqlIdentifier inputParam, SqlWhereClause whereClause)
|
||||||
|
@ -440,6 +465,30 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
throw new DocumentQueryException("Unexpected SQL select clause type: " + spec.GetType());
|
throw new DocumentQueryException("Unexpected SQL select clause type: " + spec.GetType());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private SqlGroupByClause Substitute(SqlSelectSpec spec, SqlIdentifier inputParam, SqlGroupByClause groupByClause)
|
||||||
|
{
|
||||||
|
if (groupByClause == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
SqlSelectValueSpec selectValueSpec = spec as SqlSelectValueSpec;
|
||||||
|
if (selectValueSpec != null)
|
||||||
|
{
|
||||||
|
SqlScalarExpression replaced = selectValueSpec.Expression;
|
||||||
|
SqlScalarExpression[] substitutedItems = new SqlScalarExpression[groupByClause.Expressions.Length];
|
||||||
|
for (int i = 0; i < substitutedItems.Length; ++i)
|
||||||
|
{
|
||||||
|
SqlScalarExpression substituted = SqlExpressionManipulation.Substitute(replaced, inputParam, groupByClause.Expressions[i]);
|
||||||
|
substitutedItems[i] = substituted;
|
||||||
|
}
|
||||||
|
SqlGroupByClause result = SqlGroupByClause.Create(substitutedItems);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new DocumentQueryException("Unexpected SQL select clause type: " + spec.GetType());
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determine if the current method call should create a new QueryUnderConstruction node or not.
|
/// Determine if the current method call should create a new QueryUnderConstruction node or not.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -449,10 +498,14 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
public bool ShouldBeOnNewQuery(string methodName, int argumentCount)
|
public bool ShouldBeOnNewQuery(string methodName, int argumentCount)
|
||||||
{
|
{
|
||||||
// In the LINQ provider perspective, a SQL query (without subquery) the order of the execution of the operations is:
|
// In the LINQ provider perspective, a SQL query (without subquery) the order of the execution of the operations is:
|
||||||
// Join -> Where -> Order By -> Aggregates/Distinct/Select -> Top/Offset Limit
|
// Join -> Where -> Order By -> Aggregates/Distinct/Select -> Top/Offset Limit
|
||||||
|
// | |
|
||||||
|
// |-> Group By->|
|
||||||
//
|
//
|
||||||
// The order for the corresponding LINQ operations is:
|
// The order for the corresponding LINQ operations is:
|
||||||
// SelectMany -> Where -> OrderBy -> Aggregates/Distinct/Select -> Skip/Take
|
// SelectMany -> Where -> OrderBy -> Aggregates/Distinct/Select -> Skip/Take
|
||||||
|
// | |
|
||||||
|
// |-> Group By->|
|
||||||
//
|
//
|
||||||
// In general, if an operation Op1 is being visited and the current query already has Op0 which
|
// In general, if an operation Op1 is being visited and the current query already has Op0 which
|
||||||
// appear not before Op1 in the execution order, then this Op1 needs to be in a new query. This ensures
|
// appear not before Op1 in the execution order, then this Op1 needs to be in a new query. This ensures
|
||||||
|
@ -495,7 +548,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case LinqMethods.Where:
|
case LinqMethods.Where:
|
||||||
// Where expression parameter needs to be substitued if necessary so
|
// Where expression parameter needs to be substituted if necessary so
|
||||||
// It is not needed in Select distinct because the Select distinct would have the necessary parameter name adjustment.
|
// It is not needed in Select distinct because the Select distinct would have the necessary parameter name adjustment.
|
||||||
case LinqMethods.Any:
|
case LinqMethods.Any:
|
||||||
case LinqMethods.OrderBy:
|
case LinqMethods.OrderBy:
|
||||||
|
@ -506,7 +559,16 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
// New query is needed when there is already a Take or a non-distinct Select
|
// New query is needed when there is already a Take or a non-distinct Select
|
||||||
shouldPackage = (this.topSpec != null) ||
|
shouldPackage = (this.topSpec != null) ||
|
||||||
(this.offsetSpec != null) ||
|
(this.offsetSpec != null) ||
|
||||||
(this.selectClause != null && !this.selectClause.HasDistinct);
|
(this.selectClause != null && !this.selectClause.HasDistinct) ||
|
||||||
|
(this.groupByClause != null);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LinqMethods.GroupBy:
|
||||||
|
// New query is needed when there is already a Take or a Select or a Group by clause
|
||||||
|
shouldPackage = (this.topSpec != null) ||
|
||||||
|
(this.offsetSpec != null) ||
|
||||||
|
(this.selectClause != null) ||
|
||||||
|
(this.groupByClause != null);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case LinqMethods.Skip:
|
case LinqMethods.Skip:
|
||||||
|
@ -592,6 +654,16 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
return context.CurrentQuery;
|
return context.CurrentQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public QueryUnderConstruction AddGroupByClause(SqlGroupByClause groupBy, TranslationContext context)
|
||||||
|
{
|
||||||
|
QueryUnderConstruction result = context.PackageCurrentQueryIfNeccessary();
|
||||||
|
|
||||||
|
result.groupByClause = groupBy;
|
||||||
|
foreach (Binding binding in context.CurrentSubqueryBinding.TakeBindings()) result.AddBinding(binding);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
public QueryUnderConstruction AddOffsetSpec(SqlOffsetSpec offsetSpec, TranslationContext context)
|
public QueryUnderConstruction AddOffsetSpec(SqlOffsetSpec offsetSpec, TranslationContext context)
|
||||||
{
|
{
|
||||||
QueryUnderConstruction result = context.PackageCurrentQueryIfNeccessary();
|
QueryUnderConstruction result = context.PackageCurrentQueryIfNeccessary();
|
||||||
|
@ -826,6 +898,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
private bool HasSelectAggregate()
|
private bool HasSelectAggregate()
|
||||||
{
|
{
|
||||||
string functionCallName = ((this.selectClause?.SelectSpec as SqlSelectValueSpec)?.Expression as SqlFunctionCallScalarExpression)?.Name.Value;
|
string functionCallName = ((this.selectClause?.SelectSpec as SqlSelectValueSpec)?.Expression as SqlFunctionCallScalarExpression)?.Name.Value;
|
||||||
|
|
||||||
return (functionCallName != null) &&
|
return (functionCallName != null) &&
|
||||||
((functionCallName == SqlFunctionCallScalarExpression.Names.Max) ||
|
((functionCallName == SqlFunctionCallScalarExpression.Names.Max) ||
|
||||||
(functionCallName == SqlFunctionCallScalarExpression.Names.Min) ||
|
(functionCallName == SqlFunctionCallScalarExpression.Names.Min) ||
|
||||||
|
|
|
@ -43,6 +43,16 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IDictionary<object, string> Parameters;
|
public IDictionary<object, string> Parameters;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dictionary for group by key substitution.
|
||||||
|
/// </summary>
|
||||||
|
public ParameterSubstitution GroupByKeySubstitution;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Boolean to indicate a GroupBy expression is the last expression to finished processing.
|
||||||
|
/// </summary>
|
||||||
|
public bool LastExpressionIsGroupBy;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If the FROM clause uses a parameter name, it will be substituted for the parameter used in
|
/// If the FROM clause uses a parameter name, it will be substituted for the parameter used in
|
||||||
/// the lambda expressions for the WHERE and SELECT clauses.
|
/// the lambda expressions for the WHERE and SELECT clauses.
|
||||||
|
@ -86,6 +96,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
this.subqueryBindingStack = new Stack<SubqueryBinding>();
|
this.subqueryBindingStack = new Stack<SubqueryBinding>();
|
||||||
this.Parameters = parameters;
|
this.Parameters = parameters;
|
||||||
this.clientOperation = null;
|
this.clientOperation = null;
|
||||||
|
this.LastExpressionIsGroupBy = false;
|
||||||
|
|
||||||
if (linqSerializerOptionsInternal?.CustomCosmosLinqSerializer != null)
|
if (linqSerializerOptionsInternal?.CustomCosmosLinqSerializer != null)
|
||||||
{
|
{
|
||||||
|
@ -104,6 +115,8 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
this.CosmosLinqSerializer = TranslationContext.DefaultLinqSerializer;
|
this.CosmosLinqSerializer = TranslationContext.DefaultLinqSerializer;
|
||||||
this.MemberNames = TranslationContext.DefaultMemberNames;
|
this.MemberNames = TranslationContext.DefaultMemberNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.GroupByKeySubstitution = new ParameterSubstitution();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ScalarOperationKind ClientOperation => this.clientOperation ?? ScalarOperationKind.None;
|
public ScalarOperationKind ClientOperation => this.clientOperation ?? ScalarOperationKind.None;
|
||||||
|
@ -120,17 +133,25 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
|
|
||||||
public Expression LookupSubstitution(ParameterExpression parameter)
|
public Expression LookupSubstitution(ParameterExpression parameter)
|
||||||
{
|
{
|
||||||
|
if (this.CurrentQuery.GroupByParameter != null)
|
||||||
|
{
|
||||||
|
Expression groupBySubstitutionExpression = this.GroupByKeySubstitution.Lookup(parameter);
|
||||||
|
if (groupBySubstitutionExpression != null)
|
||||||
|
{
|
||||||
|
return groupBySubstitutionExpression;
|
||||||
|
}
|
||||||
|
}
|
||||||
return this.substitutions.Lookup(parameter);
|
return this.substitutions.Lookup(parameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ParameterExpression GenFreshParameter(Type parameterType, string baseParameterName)
|
public ParameterExpression GenerateFreshParameter(Type parameterType, string baseParameterName, bool includeSuffix = true)
|
||||||
{
|
{
|
||||||
return Utilities.NewParameter(baseParameterName, parameterType, this.InScope);
|
return Utilities.NewParameter(baseParameterName, parameterType, this.InScope, includeSuffix);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Func<string, ParameterExpression> GetGenFreshParameterFunc()
|
public Func<string, ParameterExpression> GetGenFreshParameterFunc()
|
||||||
{
|
{
|
||||||
return (paramName) => this.GenFreshParameter(typeof(object), paramName);
|
return (paramName) => this.GenerateFreshParameter(typeof(object), paramName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -211,12 +232,12 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
throw new ArgumentNullException("collection");
|
throw new ArgumentNullException("collection");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.collectionStack.Add(collection);
|
if (this.CurrentQuery.GroupByParameter == null) this.collectionStack.Add(collection);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void PopCollection()
|
public void PopCollection()
|
||||||
{
|
{
|
||||||
this.collectionStack.RemoveAt(this.collectionStack.Count - 1);
|
if (this.CurrentQuery.GroupByParameter == null) this.collectionStack.RemoveAt(this.collectionStack.Count - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -226,7 +247,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
/// <param name="name">Suggested name for the input parameter.</param>
|
/// <param name="name">Suggested name for the input parameter.</param>
|
||||||
public ParameterExpression SetInputParameter(Type type, string name)
|
public ParameterExpression SetInputParameter(Type type, string name)
|
||||||
{
|
{
|
||||||
return this.CurrentQuery.fromParameters.SetInputParameter(type, name, this.InScope);
|
return this.CurrentQuery.FromParameters.SetInputParameter(type, name, this.InScope);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -237,7 +258,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
public void SetFromParameter(ParameterExpression parameter, SqlCollection collection)
|
public void SetFromParameter(ParameterExpression parameter, SqlCollection collection)
|
||||||
{
|
{
|
||||||
Binding binding = new Binding(parameter, collection, isInCollection: true);
|
Binding binding = new Binding(parameter, collection, isInCollection: true);
|
||||||
this.CurrentQuery.fromParameters.Add(binding);
|
this.CurrentQuery.FromParameters.Add(binding);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -45,15 +45,16 @@ namespace Microsoft.Azure.Cosmos.Linq
|
||||||
/// <param name="prefix">Prefix for the parameter name.</param>
|
/// <param name="prefix">Prefix for the parameter name.</param>
|
||||||
/// <param name="type">Parameter type.</param>
|
/// <param name="type">Parameter type.</param>
|
||||||
/// <param name="inScope">Names to avoid.</param>
|
/// <param name="inScope">Names to avoid.</param>
|
||||||
|
/// <param name="includeSuffix">Enable suffix to parameter name</param>
|
||||||
/// <returns>The new parameter.</returns>
|
/// <returns>The new parameter.</returns>
|
||||||
public static ParameterExpression NewParameter(string prefix, Type type, HashSet<ParameterExpression> inScope)
|
public static ParameterExpression NewParameter(string prefix, Type type, HashSet<ParameterExpression> inScope, bool includeSuffix = true)
|
||||||
{
|
{
|
||||||
int suffix = 0;
|
int suffix = 0;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
string name = prefix + suffix.ToString(CultureInfo.InvariantCulture);
|
string name = prefix + (includeSuffix ? suffix.ToString(CultureInfo.InvariantCulture) : string.Empty);
|
||||||
ParameterExpression param = Expression.Parameter(type, name);
|
ParameterExpression param = Expression.Parameter(type, name);
|
||||||
if (!inScope.Any(p => p.Name.Equals(name)))
|
if (!inScope.Any(p => p.Name.Equals(name)) || !includeSuffix)
|
||||||
{
|
{
|
||||||
inScope.Add(param);
|
inScope.Add(param);
|
||||||
return param;
|
return param;
|
||||||
|
|
|
@ -495,6 +495,7 @@ namespace Microsoft.Azure.Cosmos.SqlObjects.Visitors
|
||||||
|
|
||||||
if (sqlQuery.GroupByClause != null)
|
if (sqlQuery.GroupByClause != null)
|
||||||
{
|
{
|
||||||
|
this.WriteDelimiter(string.Empty);
|
||||||
sqlQuery.GroupByClause.Accept(this);
|
sqlQuery.GroupByClause.Accept(this);
|
||||||
this.writer.Write(" ");
|
this.writer.Write(" ");
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,449 @@
|
||||||
|
<Results>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[GroupBy Single Value Select Key]]></Description>
|
||||||
|
<Expression><![CDATA[query.GroupBy(k => k, (key, values) => key)]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[
|
||||||
|
SELECT VALUE root
|
||||||
|
FROM root
|
||||||
|
GROUP BY root ]]></SqlQuery>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[GroupBy Single Value Select Key]]></Description>
|
||||||
|
<Expression><![CDATA[query.GroupBy(k => k.Id, (key, values) => key)]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[
|
||||||
|
SELECT VALUE root["id"]
|
||||||
|
FROM root
|
||||||
|
GROUP BY root["id"] ]]></SqlQuery>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[GroupBy Single Value Select Key Alias]]></Description>
|
||||||
|
<Expression><![CDATA[query.GroupBy(k => k.Id, (stringField, values) => stringField)]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[
|
||||||
|
SELECT VALUE root["id"]
|
||||||
|
FROM root
|
||||||
|
GROUP BY root["id"] ]]></SqlQuery>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[GroupBy Single Value With Min]]></Description>
|
||||||
|
<Expression><![CDATA[query.GroupBy(k => k.Id, (key, values) => values.Min(value => value.Int))]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[
|
||||||
|
SELECT VALUE MIN(root["Int"])
|
||||||
|
FROM root
|
||||||
|
GROUP BY root["id"] ]]></SqlQuery>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[GroupBy Single Value With Max]]></Description>
|
||||||
|
<Expression><![CDATA[query.GroupBy(k => k.Id, (key, values) => values.Max(value => value.Int))]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[
|
||||||
|
SELECT VALUE MAX(root["Int"])
|
||||||
|
FROM root
|
||||||
|
GROUP BY root["id"] ]]></SqlQuery>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[GroupBy Single Value With Count]]></Description>
|
||||||
|
<Expression><![CDATA[query.GroupBy(k => k.Id, (key, values) => values.Count())]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[
|
||||||
|
SELECT VALUE COUNT(1)
|
||||||
|
FROM root
|
||||||
|
GROUP BY root["id"] ]]></SqlQuery>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[GroupBy Single Value With Average]]></Description>
|
||||||
|
<Expression><![CDATA[query.GroupBy(k => k.Id, (key, values) => values.Average(value => value.Int))]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[
|
||||||
|
SELECT VALUE AVG(root["Int"])
|
||||||
|
FROM root
|
||||||
|
GROUP BY root["id"] ]]></SqlQuery>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[GroupBy Single Value With Min]]></Description>
|
||||||
|
<Expression><![CDATA[query.GroupBy(k => k.Int, (key, values) => values.Min())]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[
|
||||||
|
SELECT VALUE MIN(root)
|
||||||
|
FROM root
|
||||||
|
GROUP BY root["Int"] ]]></SqlQuery>
|
||||||
|
<ErrorMessage><![CDATA[At least one object must implement IComparable.]]></ErrorMessage>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[GroupBy Single Value With Max]]></Description>
|
||||||
|
<Expression><![CDATA[query.GroupBy(k => k.Int, (key, values) => values.Max())]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[
|
||||||
|
SELECT VALUE MAX(root)
|
||||||
|
FROM root
|
||||||
|
GROUP BY root["Int"] ]]></SqlQuery>
|
||||||
|
<ErrorMessage><![CDATA[At least one object must implement IComparable.]]></ErrorMessage>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[GroupBy Single Value With Min]]></Description>
|
||||||
|
<Expression><![CDATA[query.GroupBy(k => k.Int, (key, values) => "string")]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[
|
||||||
|
SELECT VALUE "string"
|
||||||
|
FROM root
|
||||||
|
GROUP BY root["Int"] ]]></SqlQuery>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[GroupBy Single Value With Count]]></Description>
|
||||||
|
<Expression><![CDATA[query.GroupBy(k => k.Id)]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[]]></SqlQuery>
|
||||||
|
<ErrorMessage><![CDATA[Incorrect number of arguments for method 'GroupBy'. Expected '3' but received '2'.]]></ErrorMessage>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[GroupBy Single Value With Min]]></Description>
|
||||||
|
<Expression><![CDATA[query.GroupBy(k => k.Int, k2 => k2.Int, (key, values) => "string")]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[]]></SqlQuery>
|
||||||
|
<ErrorMessage><![CDATA[Incorrect number of arguments for method 'GroupBy'. Expected '3' but received '4'.]]></ErrorMessage>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[GroupBy Single Value With Count]]></Description>
|
||||||
|
<Expression><![CDATA[query.GroupBy(k => k.Id, (key, values) => values.Select(value => value.Int))]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[]]></SqlQuery>
|
||||||
|
<ErrorMessage><![CDATA[Method 'Select' is not supported.]]></ErrorMessage>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[GroupBy Single Value With Count]]></Description>
|
||||||
|
<Expression><![CDATA[query.GroupBy(k => k.Id, (key, values) => values.OrderBy(f => f.FamilyId))]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[]]></SqlQuery>
|
||||||
|
<ErrorMessage><![CDATA[Method 'OrderBy' is not supported.]]></ErrorMessage>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[GroupBy Single Value With Min]]></Description>
|
||||||
|
<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>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[Select + GroupBy]]></Description>
|
||||||
|
<Expression><![CDATA[query.Select(x => x.Id).GroupBy(k => k, (key, values) => key)]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[
|
||||||
|
SELECT VALUE r0
|
||||||
|
FROM (
|
||||||
|
SELECT VALUE root["id"]
|
||||||
|
FROM root) AS r0
|
||||||
|
GROUP BY r0
|
||||||
|
]]></SqlQuery>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[Select + GroupBy 2]]></Description>
|
||||||
|
<Expression><![CDATA[query.Select(x => new AnonymousType(Id1 = x.Id, family1 = x.FamilyId, childrenN1 = x.Children)).GroupBy(k => k.family1, (key, values) => key)]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[
|
||||||
|
SELECT VALUE r0["family1"]
|
||||||
|
FROM (
|
||||||
|
SELECT VALUE {"Id1": root["id"], "family1": root["FamilyId"], "childrenN1": root["Children"]}
|
||||||
|
FROM root) AS r0
|
||||||
|
GROUP BY r0["family1"]
|
||||||
|
]]></SqlQuery>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[SelectMany + GroupBy]]></Description>
|
||||||
|
<Expression><![CDATA[query.SelectMany(x => x.Children).GroupBy(k => k.Grade, (key, values) => key)]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[
|
||||||
|
SELECT VALUE x0["Grade"]
|
||||||
|
FROM root
|
||||||
|
JOIN x0 IN root["Children"]
|
||||||
|
GROUP BY x0["Grade"] ]]></SqlQuery>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[SelectMany + GroupBy 2]]></Description>
|
||||||
|
<Expression><![CDATA[query.SelectMany(f => f.Children).Where(c => (c.Pets.Count() > 0)).SelectMany(c => c.Pets.Select(p => p.GivenName)).GroupBy(k => k, (key, values) => key)]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[
|
||||||
|
SELECT VALUE r0
|
||||||
|
FROM (
|
||||||
|
SELECT VALUE p0["GivenName"]
|
||||||
|
FROM root
|
||||||
|
JOIN f0 IN root["Children"]
|
||||||
|
JOIN p0 IN f0["Pets"]
|
||||||
|
WHERE (ARRAY_LENGTH(f0["Pets"]) > 0)) AS r0
|
||||||
|
GROUP BY r0
|
||||||
|
]]></SqlQuery>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[Skip + GroupBy]]></Description>
|
||||||
|
<Expression><![CDATA[query.Skip(10).GroupBy(k => k.Id, (key, values) => key)]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[
|
||||||
|
SELECT VALUE r0["id"]
|
||||||
|
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":56,"end":82},"code":"SC2204","message":"'OFFSET LIMIT' clause is not supported in subqueries."}]},0x800A0B00]]></ErrorMessage>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[Take + GroupBy]]></Description>
|
||||||
|
<Expression><![CDATA[query.Take(10).GroupBy(k => k.Id, (key, values) => key)]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[
|
||||||
|
SELECT VALUE r0["id"]
|
||||||
|
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":35,"end":41},"code":"SC2203","message":"'TOP' is not supported in subqueries."}]},0x800A0B00]]></ErrorMessage>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[Skip + Take + GroupBy]]></Description>
|
||||||
|
<Expression><![CDATA[query.Skip(10).Take(10).GroupBy(k => k.Id, (key, values) => key)]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[
|
||||||
|
SELECT VALUE r0["id"]
|
||||||
|
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":56,"end":74},"code":"SC2204","message":"'OFFSET LIMIT' clause is not supported in subqueries."}]},0x800A0B00]]></ErrorMessage>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[Filter + GroupBy]]></Description>
|
||||||
|
<Expression><![CDATA[query.Where(x => (x.Id != "a")).GroupBy(k => k.Id, (key, values) => key)]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[
|
||||||
|
SELECT VALUE root["id"]
|
||||||
|
FROM root
|
||||||
|
WHERE (root["id"] != "a")
|
||||||
|
GROUP BY root["id"] ]]></SqlQuery>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[OrderBy + GroupBy]]></Description>
|
||||||
|
<Expression><![CDATA[query.OrderBy(x => x.Int).GroupBy(k => k.Id, (key, values) => key)]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[
|
||||||
|
SELECT VALUE root["id"]
|
||||||
|
FROM root
|
||||||
|
GROUP BY root["id"]
|
||||||
|
ORDER BY root["Int"] ASC]]></SqlQuery>
|
||||||
|
<ErrorMessage><![CDATA[Status Code: BadRequest,{"errors":[{"severity":"Error","location":{"start":64,"end":75},"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>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[OrderBy Descending + GroupBy]]></Description>
|
||||||
|
<Expression><![CDATA[query.OrderByDescending(x => x.Id).GroupBy(k => k.Id, (key, values) => key)]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[
|
||||||
|
SELECT VALUE root["id"]
|
||||||
|
FROM root
|
||||||
|
GROUP BY root["id"]
|
||||||
|
ORDER BY root["id"] DESC]]></SqlQuery>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[Combination + GroupBy]]></Description>
|
||||||
|
<Expression><![CDATA[query.Where(x => (x.Id != "a")).OrderBy(x => x.Id).GroupBy(k => k.Id, (key, values) => key)]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[
|
||||||
|
SELECT VALUE root["id"]
|
||||||
|
FROM root
|
||||||
|
WHERE (root["id"] != "a")
|
||||||
|
GROUP BY root["id"]
|
||||||
|
ORDER BY root["id"] ASC]]></SqlQuery>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[Combination 2 + GroupBy]]></Description>
|
||||||
|
<Expression><![CDATA[query.Where(x => (x.Id != "a")).Where(x => (x.Children.Min(y => y.Grade) > 10)).GroupBy(k => k.Id, (key, values) => key)]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[
|
||||||
|
SELECT VALUE root["id"]
|
||||||
|
FROM root
|
||||||
|
JOIN (
|
||||||
|
SELECT VALUE ARRAY(
|
||||||
|
SELECT VALUE MIN(y0["Grade"])
|
||||||
|
FROM root
|
||||||
|
JOIN y0 IN root["Children"])) AS v0
|
||||||
|
WHERE ((root["id"] != "a") AND (v0[0] > 10))
|
||||||
|
GROUP BY root["id"]
|
||||||
|
]]></SqlQuery>
|
||||||
|
<ErrorMessage><![CDATA[Sequence contains no elements]]></ErrorMessage>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[GroupBy + Select]]></Description>
|
||||||
|
<Expression><![CDATA[query.GroupBy(k => k.Id, (key, values) => key).Select(x => x)]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[]]></SqlQuery>
|
||||||
|
<ErrorMessage><![CDATA[Group By cannot be followed by other methods]]></ErrorMessage>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[GroupBy + Skip]]></Description>
|
||||||
|
<Expression><![CDATA[query.GroupBy(k => k.Id, (key, values) => key).Skip(10)]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[]]></SqlQuery>
|
||||||
|
<ErrorMessage><![CDATA[Group By cannot be followed by other methods]]></ErrorMessage>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[GroupBy + Take]]></Description>
|
||||||
|
<Expression><![CDATA[query.GroupBy(k => k.Id, (key, values) => key).Take(10)]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[]]></SqlQuery>
|
||||||
|
<ErrorMessage><![CDATA[Group By cannot be followed by other methods]]></ErrorMessage>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[GroupBy + Skip + Take]]></Description>
|
||||||
|
<Expression><![CDATA[query.GroupBy(k => k.Id, (key, values) => key).Skip(10).Take(10)]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[]]></SqlQuery>
|
||||||
|
<ErrorMessage><![CDATA[Group By cannot be followed by other methods]]></ErrorMessage>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[GroupBy + Filter]]></Description>
|
||||||
|
<Expression><![CDATA[query.GroupBy(k => k.Id, (key, values) => key).Where(x => (x == "a"))]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[]]></SqlQuery>
|
||||||
|
<ErrorMessage><![CDATA[Group By cannot be followed by other methods]]></ErrorMessage>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[GroupBy + OrderBy]]></Description>
|
||||||
|
<Expression><![CDATA[query.GroupBy(k => k.Id, (key, values) => key).OrderBy(x => x)]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[]]></SqlQuery>
|
||||||
|
<ErrorMessage><![CDATA[Group By cannot be followed by other methods]]></ErrorMessage>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[GroupBy + OrderBy Descending]]></Description>
|
||||||
|
<Expression><![CDATA[query.GroupBy(k => k.Id, (key, values) => key).OrderByDescending(x => x)]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[]]></SqlQuery>
|
||||||
|
<ErrorMessage><![CDATA[Group By cannot be followed by other methods]]></ErrorMessage>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[GroupBy + Combination]]></Description>
|
||||||
|
<Expression><![CDATA[query.GroupBy(k => k.Id, (key, values) => key).Where(x => (x == "a")).Skip(10).Take(10)]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[]]></SqlQuery>
|
||||||
|
<ErrorMessage><![CDATA[Group By cannot be followed by other methods]]></ErrorMessage>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
<Result>
|
||||||
|
<Input>
|
||||||
|
<Description><![CDATA[GroupBy + GroupBy]]></Description>
|
||||||
|
<Expression><![CDATA[query.GroupBy(k => k.Id, (key, values) => key).GroupBy(k => k, (key, values) => key)]]></Expression>
|
||||||
|
</Input>
|
||||||
|
<Output>
|
||||||
|
<SqlQuery><![CDATA[]]></SqlQuery>
|
||||||
|
<ErrorMessage><![CDATA[Group By cannot be followed by other methods]]></ErrorMessage>
|
||||||
|
</Output>
|
||||||
|
</Result>
|
||||||
|
</Results>
|
|
@ -697,6 +697,155 @@ namespace Microsoft.Azure.Cosmos.Services.Management.Tests.LinqProviderTests
|
||||||
this.ExecuteTestSuite(inputs);
|
this.ExecuteTestSuite(inputs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestGroupByTranslation()
|
||||||
|
{
|
||||||
|
List<LinqTestInput> inputs = new List<LinqTestInput>();
|
||||||
|
inputs.Add(new LinqTestInput("GroupBy Single Value Select Key", b => getQuery(b).GroupBy(k => k /*keySelector*/,
|
||||||
|
(key, values) => key /*return the group by key */)));
|
||||||
|
inputs.Add(new LinqTestInput("GroupBy Single Value Select Key", b => getQuery(b).GroupBy(k => k.Id /*keySelector*/,
|
||||||
|
(key, values) => key /*return the group by key */)));
|
||||||
|
inputs.Add(new LinqTestInput("GroupBy Single Value Select Key Alias", b => getQuery(b).GroupBy(k => k.Id /*keySelector*/,
|
||||||
|
(stringField, values) => stringField /*return the group by key */)));
|
||||||
|
|
||||||
|
|
||||||
|
inputs.Add(new LinqTestInput("GroupBy Single Value With Min", b => getQuery(b).GroupBy(k => k.Id /*keySelector*/,
|
||||||
|
(key, values) => values.Min(value => value.Int) /*return the Min of each group */)));
|
||||||
|
inputs.Add(new LinqTestInput("GroupBy Single Value With Max", b => getQuery(b).GroupBy(k => k.Id /*keySelector*/,
|
||||||
|
(key, values) => values.Max(value => value.Int) /*return the Max of each group */)));
|
||||||
|
inputs.Add(new LinqTestInput("GroupBy Single Value With Count", b => getQuery(b).GroupBy(k => k.Id /*keySelector*/,
|
||||||
|
(key, values) => values.Count() /*return the Count of each group */)));
|
||||||
|
inputs.Add(new LinqTestInput("GroupBy Single Value With Average", b => getQuery(b).GroupBy(k => k.Id /*keySelector*/,
|
||||||
|
(key, values) => values.Average(value => value.Int) /*return the Count of each group */)));
|
||||||
|
|
||||||
|
// 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
|
||||||
|
inputs.Add(new LinqTestInput("GroupBy Single Value With Min", b => getQuery(b).GroupBy(k => k.Int /*keySelector*/,
|
||||||
|
(key, values) => values.Min() /*return the Min of each group */)));
|
||||||
|
inputs.Add(new LinqTestInput("GroupBy Single Value With Max", b => getQuery(b).GroupBy(k => k.Int /*keySelector*/,
|
||||||
|
(key, values) => values.Max() /*return the Max of each group */)));
|
||||||
|
|
||||||
|
// Unsupported node type
|
||||||
|
inputs.Add(new LinqTestInput("GroupBy Single Value With Min", b => getQuery(b).GroupBy(k => k.Int /*keySelector*/,
|
||||||
|
(key, values) => "string" /* Unsupported Nodetype*/ )));
|
||||||
|
|
||||||
|
// Incorrect number of arguments
|
||||||
|
inputs.Add(new LinqTestInput("GroupBy Single Value With Count", b => getQuery(b).GroupBy(k => k.Id)));
|
||||||
|
inputs.Add(new LinqTestInput("GroupBy Single Value With Min", b => getQuery(b).GroupBy(
|
||||||
|
k => k.Int,
|
||||||
|
k2 => k2.Int,
|
||||||
|
(key, values) => "string" /* Unsupported Nodetype*/ )));
|
||||||
|
|
||||||
|
// Non-aggregate method calls
|
||||||
|
inputs.Add(new LinqTestInput("GroupBy Single Value With Count", b => getQuery(b).GroupBy(k => k.Id /*keySelector*/,
|
||||||
|
(key, values) => values.Select(value => value.Int) /*Not an aggregate*/)));
|
||||||
|
inputs.Add(new LinqTestInput("GroupBy Single Value With Count", b => getQuery(b).GroupBy(k => k.Id /*keySelector*/,
|
||||||
|
(key, values) => values.OrderBy(f => f.FamilyId) /*Not an aggregate*/)));
|
||||||
|
|
||||||
|
// 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 */)));
|
||||||
|
|
||||||
|
// Other methods followed by GroupBy
|
||||||
|
|
||||||
|
inputs.Add(new LinqTestInput("Select + GroupBy", b => getQuery(b)
|
||||||
|
.Select(x => x.Id)
|
||||||
|
.GroupBy(k => k /*keySelector*/, (key, values) => key /*return the group by key */)));
|
||||||
|
|
||||||
|
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.family1 /*keySelector*/, (key, values) => key /*return the group by key */)));
|
||||||
|
|
||||||
|
inputs.Add(new LinqTestInput("SelectMany + GroupBy", b => getQuery(b)
|
||||||
|
.SelectMany(x => x.Children)
|
||||||
|
.GroupBy(k => k.Grade /*keySelector*/, (key, values) => key /*return the group by key */)));
|
||||||
|
|
||||||
|
inputs.Add(new LinqTestInput("SelectMany + GroupBy 2", b => getQuery(b)
|
||||||
|
.SelectMany(f => f.Children)
|
||||||
|
.Where(c => c.Pets.Count() > 0)
|
||||||
|
.SelectMany(c => c.Pets.Select(p => p.GivenName))
|
||||||
|
.GroupBy(k => k /*keySelector*/, (key, values) => key /*return the group by key */)));
|
||||||
|
|
||||||
|
inputs.Add(new LinqTestInput("Skip + GroupBy", b => getQuery(b)
|
||||||
|
.Skip(10)
|
||||||
|
.GroupBy(k => k.Id /*keySelector*/, (key, values) => key /*return the group by key */)));
|
||||||
|
|
||||||
|
inputs.Add(new LinqTestInput("Take + GroupBy", b => getQuery(b)
|
||||||
|
.Take(10)
|
||||||
|
.GroupBy(k => k.Id /*keySelector*/, (key, values) => key /*return the group by key */)));
|
||||||
|
|
||||||
|
inputs.Add(new LinqTestInput("Skip + Take + GroupBy", b => getQuery(b)
|
||||||
|
.Skip(10).Take(10)
|
||||||
|
.GroupBy(k => k.Id /*keySelector*/, (key, values) => key /*return the group by key */)));
|
||||||
|
|
||||||
|
inputs.Add(new LinqTestInput("Filter + GroupBy", b => getQuery(b)
|
||||||
|
.Where(x => x.Id != "a")
|
||||||
|
.GroupBy(k => k.Id /*keySelector*/, (key, values) => key /*return the group by key */)));
|
||||||
|
|
||||||
|
// should this become a subquery with order by then group by?
|
||||||
|
inputs.Add(new LinqTestInput("OrderBy + GroupBy", b => getQuery(b)
|
||||||
|
.OrderBy(x => x.Int)
|
||||||
|
.GroupBy(k => k.Id /*keySelector*/, (key, values) => key /*return the group by key */)));
|
||||||
|
|
||||||
|
inputs.Add(new LinqTestInput("OrderBy Descending + GroupBy", b => getQuery(b)
|
||||||
|
.OrderByDescending(x => x.Id)
|
||||||
|
.GroupBy(k => k.Id /*keySelector*/, (key, values) => key /*return the group by key */)));
|
||||||
|
|
||||||
|
inputs.Add(new LinqTestInput("Combination + GroupBy", b => getQuery(b)
|
||||||
|
.Where(x => x.Id != "a")
|
||||||
|
.OrderBy(x => x.Id)
|
||||||
|
.GroupBy(k => k.Id /*keySelector*/, (key, values) => key /*return the group by key */)));
|
||||||
|
|
||||||
|
// The result for this is not correct yet - the select clause is wrong
|
||||||
|
inputs.Add(new LinqTestInput("Combination 2 + GroupBy", b => getQuery(b)
|
||||||
|
.Where(x => x.Id != "a")
|
||||||
|
.Where(x => x.Children.Min(y => y.Grade) > 10)
|
||||||
|
.GroupBy(k => k.Id /*keySelector*/, (key, values) => key /*return the group by key */)));
|
||||||
|
|
||||||
|
// GroupBy followed by other methods
|
||||||
|
inputs.Add(new LinqTestInput("GroupBy + Select", b => getQuery(b)
|
||||||
|
.GroupBy(k => k.Id /*keySelector*/, (key, values) => key /*return the group by key */)
|
||||||
|
.Select(x => x)));
|
||||||
|
|
||||||
|
//We should support skip take
|
||||||
|
inputs.Add(new LinqTestInput("GroupBy + Skip", b => getQuery(b)
|
||||||
|
.GroupBy(k => k.Id /*keySelector*/, (key, values) => key /*return the group by key */)
|
||||||
|
.Skip(10)));
|
||||||
|
|
||||||
|
inputs.Add(new LinqTestInput("GroupBy + Take", b => getQuery(b)
|
||||||
|
.GroupBy(k => k.Id /*keySelector*/, (key, values) => key /*return the group by key */)
|
||||||
|
.Take(10)));
|
||||||
|
|
||||||
|
inputs.Add(new LinqTestInput("GroupBy + Skip + Take", b => getQuery(b)
|
||||||
|
.GroupBy(k => k.Id /*keySelector*/, (key, values) => key /*return the group by key */)
|
||||||
|
.Skip(10).Take(10)));
|
||||||
|
|
||||||
|
inputs.Add(new LinqTestInput("GroupBy + Filter", b => getQuery(b)
|
||||||
|
.GroupBy(k => k.Id /*keySelector*/, (key, values) => key /*return the group by key */)
|
||||||
|
.Where(x => x == "a")));
|
||||||
|
|
||||||
|
inputs.Add(new LinqTestInput("GroupBy + OrderBy", b => getQuery(b)
|
||||||
|
.GroupBy(k => k.Id /*keySelector*/, (key, values) => key /*return the group by key */)
|
||||||
|
.OrderBy(x => x)));
|
||||||
|
|
||||||
|
inputs.Add(new LinqTestInput("GroupBy + OrderBy Descending", b => getQuery(b)
|
||||||
|
.GroupBy(k => k.Id /*keySelector*/, (key, values) => key /*return the group by key */)
|
||||||
|
.OrderByDescending(x => x)));
|
||||||
|
|
||||||
|
inputs.Add(new LinqTestInput("GroupBy + Combination", b => getQuery(b)
|
||||||
|
.GroupBy(k => k.Id /*keySelector*/, (key, values) => key /*return the group by key */)
|
||||||
|
.Where(x => x == "a").Skip(10).Take(10)));
|
||||||
|
|
||||||
|
inputs.Add(new LinqTestInput("GroupBy + GroupBy", b => getQuery(b)
|
||||||
|
.GroupBy(k => k.Id /*keySelector*/, (key, values) => key /*return the group by key */)
|
||||||
|
.GroupBy(k => k /*keySelector*/, (key, values) => key /*return the group by key */)));
|
||||||
|
|
||||||
|
this.ExecuteTestSuite(inputs);
|
||||||
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[Ignore]
|
[Ignore]
|
||||||
public void DebuggingTest()
|
public void DebuggingTest()
|
||||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -193,6 +193,9 @@
|
||||||
<Content Include="BaselineTest\TestBaseline\LinqGeneralBaselineTests.ValidateLinqQueries.xml">
|
<Content Include="BaselineTest\TestBaseline\LinqGeneralBaselineTests.ValidateLinqQueries.xml">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
|
<Content Include="BaselineTest\TestBaseline\LinqGeneralBaselineTests.TestGroupByTranslation.xml">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
<Content Include="BaselineTest\TestBaseline\LinqGeneralBaselineTests.TestOrderByTranslation.xml">
|
<Content Include="BaselineTest\TestBaseline\LinqGeneralBaselineTests.TestOrderByTranslation.xml">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<Query><![CDATA[SELECT * GROUP BY 1]]></Query>
|
<Query><![CDATA[SELECT * GROUP BY 1]]></Query>
|
||||||
</Input>
|
</Input>
|
||||||
<Output>
|
<Output>
|
||||||
<ParsedQuery><![CDATA[SELECT *GROUP BY 1 ]]></ParsedQuery>
|
<ParsedQuery><![CDATA[SELECT * GROUP BY 1 ]]></ParsedQuery>
|
||||||
</Output>
|
</Output>
|
||||||
</Result>
|
</Result>
|
||||||
<Result>
|
<Result>
|
||||||
|
@ -14,7 +14,7 @@
|
||||||
<Query><![CDATA[SELECT * GrOuP By 1]]></Query>
|
<Query><![CDATA[SELECT * GrOuP By 1]]></Query>
|
||||||
</Input>
|
</Input>
|
||||||
<Output>
|
<Output>
|
||||||
<ParsedQuery><![CDATA[SELECT *GROUP BY 1 ]]></ParsedQuery>
|
<ParsedQuery><![CDATA[SELECT * GROUP BY 1 ]]></ParsedQuery>
|
||||||
</Output>
|
</Output>
|
||||||
</Result>
|
</Result>
|
||||||
<Result>
|
<Result>
|
||||||
|
@ -23,7 +23,7 @@
|
||||||
<Query><![CDATA[SELECT * GROUP BY 1, 2, 3]]></Query>
|
<Query><![CDATA[SELECT * GROUP BY 1, 2, 3]]></Query>
|
||||||
</Input>
|
</Input>
|
||||||
<Output>
|
<Output>
|
||||||
<ParsedQuery><![CDATA[SELECT *GROUP BY 1, 2, 3 ]]></ParsedQuery>
|
<ParsedQuery><![CDATA[SELECT * GROUP BY 1, 2, 3 ]]></ParsedQuery>
|
||||||
</Output>
|
</Output>
|
||||||
</Result>
|
</Result>
|
||||||
<Result>
|
<Result>
|
||||||
|
|
|
@ -989,16 +989,17 @@ OFFSET 0 LIMIT 0
|
||||||
}]]></SqlObject>
|
}]]></SqlObject>
|
||||||
</Input>
|
</Input>
|
||||||
<Output>
|
<Output>
|
||||||
<TextOutput><![CDATA[SELECT * FROM inputPathCollection["somePath"] AS some alias WHERE ("this path" < 42)GROUP BY "some"["random"]["path"][42] ORDER BY "some"["random"]["path"][42] ASC OFFSET 0 LIMIT 0]]></TextOutput>
|
<TextOutput><![CDATA[SELECT * FROM inputPathCollection["somePath"] AS some alias WHERE ("this path" < 42) GROUP BY "some"["random"]["path"][42] ORDER BY "some"["random"]["path"][42] ASC OFFSET 0 LIMIT 0]]></TextOutput>
|
||||||
<PrettyPrint><![CDATA[
|
<PrettyPrint><![CDATA[
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM inputPathCollection["somePath"] AS some alias
|
FROM inputPathCollection["somePath"] AS some alias
|
||||||
WHERE ("this path" < 42)GROUP BY "some"["random"]["path"][42]
|
WHERE ("this path" < 42)
|
||||||
|
GROUP BY "some"["random"]["path"][42]
|
||||||
ORDER BY "some"["random"]["path"][42] ASC
|
ORDER BY "some"["random"]["path"][42] ASC
|
||||||
OFFSET 0 LIMIT 0
|
OFFSET 0 LIMIT 0
|
||||||
]]></PrettyPrint>
|
]]></PrettyPrint>
|
||||||
<HashCode>-245344741</HashCode>
|
<HashCode>-245344741</HashCode>
|
||||||
<ObfusctedQuery><![CDATA[SELECT * FROM ident1__19["str1"] AS ident2__10 WHERE ("str2" < 42)GROUP BY "str3"["str4"]["str5"][42] ORDER BY "str3"["str4"]["str5"][42] ASC OFFSET 0 LIMIT 0]]></ObfusctedQuery>
|
<ObfusctedQuery><![CDATA[SELECT * FROM ident1__19["str1"] AS ident2__10 WHERE ("str2" < 42) GROUP BY "str3"["str4"]["str5"][42] ORDER BY "str3"["str4"]["str5"][42] ASC OFFSET 0 LIMIT 0]]></ObfusctedQuery>
|
||||||
</Output>
|
</Output>
|
||||||
</Result>
|
</Result>
|
||||||
<Result>
|
<Result>
|
||||||
|
@ -1127,18 +1128,19 @@ OFFSET 0 LIMIT 0
|
||||||
}]]></SqlObject>
|
}]]></SqlObject>
|
||||||
</Input>
|
</Input>
|
||||||
<Output>
|
<Output>
|
||||||
<TextOutput><![CDATA[(SELECT * FROM inputPathCollection["somePath"] AS some alias WHERE ("this path" < 42)GROUP BY "some"["random"]["path"][42] ORDER BY "some"["random"]["path"][42] ASC OFFSET 0 LIMIT 0)]]></TextOutput>
|
<TextOutput><![CDATA[(SELECT * FROM inputPathCollection["somePath"] AS some alias WHERE ("this path" < 42) GROUP BY "some"["random"]["path"][42] ORDER BY "some"["random"]["path"][42] ASC OFFSET 0 LIMIT 0)]]></TextOutput>
|
||||||
<PrettyPrint><![CDATA[
|
<PrettyPrint><![CDATA[
|
||||||
(
|
(
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM inputPathCollection["somePath"] AS some alias
|
FROM inputPathCollection["somePath"] AS some alias
|
||||||
WHERE ("this path" < 42)GROUP BY "some"["random"]["path"][42]
|
WHERE ("this path" < 42)
|
||||||
|
GROUP BY "some"["random"]["path"][42]
|
||||||
ORDER BY "some"["random"]["path"][42] ASC
|
ORDER BY "some"["random"]["path"][42] ASC
|
||||||
OFFSET 0 LIMIT 0
|
OFFSET 0 LIMIT 0
|
||||||
)
|
)
|
||||||
]]></PrettyPrint>
|
]]></PrettyPrint>
|
||||||
<HashCode>51808704</HashCode>
|
<HashCode>51808704</HashCode>
|
||||||
<ObfusctedQuery><![CDATA[(SELECT * FROM ident1__19["str1"] AS ident2__10 WHERE ("str2" < 42)GROUP BY "str3"["str4"]["str5"][42] ORDER BY "str3"["str4"]["str5"][42] ASC OFFSET 0 LIMIT 0)]]></ObfusctedQuery>
|
<ObfusctedQuery><![CDATA[(SELECT * FROM ident1__19["str1"] AS ident2__10 WHERE ("str2" < 42) GROUP BY "str3"["str4"]["str5"][42] ORDER BY "str3"["str4"]["str5"][42] ASC OFFSET 0 LIMIT 0)]]></ObfusctedQuery>
|
||||||
</Output>
|
</Output>
|
||||||
</Result>
|
</Result>
|
||||||
<Result>
|
<Result>
|
||||||
|
@ -1267,18 +1269,19 @@ OFFSET 0 LIMIT 0
|
||||||
}]]></SqlObject>
|
}]]></SqlObject>
|
||||||
</Input>
|
</Input>
|
||||||
<Output>
|
<Output>
|
||||||
<TextOutput><![CDATA[ARRAY(SELECT * FROM inputPathCollection["somePath"] AS some alias WHERE ("this path" < 42)GROUP BY "some"["random"]["path"][42] ORDER BY "some"["random"]["path"][42] ASC OFFSET 0 LIMIT 0)]]></TextOutput>
|
<TextOutput><![CDATA[ARRAY(SELECT * FROM inputPathCollection["somePath"] AS some alias WHERE ("this path" < 42) GROUP BY "some"["random"]["path"][42] ORDER BY "some"["random"]["path"][42] ASC OFFSET 0 LIMIT 0)]]></TextOutput>
|
||||||
<PrettyPrint><![CDATA[
|
<PrettyPrint><![CDATA[
|
||||||
ARRAY(
|
ARRAY(
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM inputPathCollection["somePath"] AS some alias
|
FROM inputPathCollection["somePath"] AS some alias
|
||||||
WHERE ("this path" < 42)GROUP BY "some"["random"]["path"][42]
|
WHERE ("this path" < 42)
|
||||||
|
GROUP BY "some"["random"]["path"][42]
|
||||||
ORDER BY "some"["random"]["path"][42] ASC
|
ORDER BY "some"["random"]["path"][42] ASC
|
||||||
OFFSET 0 LIMIT 0
|
OFFSET 0 LIMIT 0
|
||||||
)
|
)
|
||||||
]]></PrettyPrint>
|
]]></PrettyPrint>
|
||||||
<HashCode>-1922520573</HashCode>
|
<HashCode>-1922520573</HashCode>
|
||||||
<ObfusctedQuery><![CDATA[ARRAY(SELECT * FROM ident1__19["str1"] AS ident2__10 WHERE ("str2" < 42)GROUP BY "str3"["str4"]["str5"][42] ORDER BY "str3"["str4"]["str5"][42] ASC OFFSET 0 LIMIT 0)]]></ObfusctedQuery>
|
<ObfusctedQuery><![CDATA[ARRAY(SELECT * FROM ident1__19["str1"] AS ident2__10 WHERE ("str2" < 42) GROUP BY "str3"["str4"]["str5"][42] ORDER BY "str3"["str4"]["str5"][42] ASC OFFSET 0 LIMIT 0)]]></ObfusctedQuery>
|
||||||
</Output>
|
</Output>
|
||||||
</Result>
|
</Result>
|
||||||
<Result>
|
<Result>
|
||||||
|
@ -1407,18 +1410,19 @@ ARRAY(
|
||||||
}]]></SqlObject>
|
}]]></SqlObject>
|
||||||
</Input>
|
</Input>
|
||||||
<Output>
|
<Output>
|
||||||
<TextOutput><![CDATA[EXISTS(SELECT * FROM inputPathCollection["somePath"] AS some alias WHERE ("this path" < 42)GROUP BY "some"["random"]["path"][42] ORDER BY "some"["random"]["path"][42] ASC OFFSET 0 LIMIT 0)]]></TextOutput>
|
<TextOutput><![CDATA[EXISTS(SELECT * FROM inputPathCollection["somePath"] AS some alias WHERE ("this path" < 42) GROUP BY "some"["random"]["path"][42] ORDER BY "some"["random"]["path"][42] ASC OFFSET 0 LIMIT 0)]]></TextOutput>
|
||||||
<PrettyPrint><![CDATA[
|
<PrettyPrint><![CDATA[
|
||||||
EXISTS(
|
EXISTS(
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM inputPathCollection["somePath"] AS some alias
|
FROM inputPathCollection["somePath"] AS some alias
|
||||||
WHERE ("this path" < 42)GROUP BY "some"["random"]["path"][42]
|
WHERE ("this path" < 42)
|
||||||
|
GROUP BY "some"["random"]["path"][42]
|
||||||
ORDER BY "some"["random"]["path"][42] ASC
|
ORDER BY "some"["random"]["path"][42] ASC
|
||||||
OFFSET 0 LIMIT 0
|
OFFSET 0 LIMIT 0
|
||||||
)
|
)
|
||||||
]]></PrettyPrint>
|
]]></PrettyPrint>
|
||||||
<HashCode>1317938775</HashCode>
|
<HashCode>1317938775</HashCode>
|
||||||
<ObfusctedQuery><![CDATA[EXISTS(SELECT * FROM ident1__19["str1"] AS ident2__10 WHERE ("str2" < 42)GROUP BY "str3"["str4"]["str5"][42] ORDER BY "str3"["str4"]["str5"][42] ASC OFFSET 0 LIMIT 0)]]></ObfusctedQuery>
|
<ObfusctedQuery><![CDATA[EXISTS(SELECT * FROM ident1__19["str1"] AS ident2__10 WHERE ("str2" < 42) GROUP BY "str3"["str4"]["str5"][42] ORDER BY "str3"["str4"]["str5"][42] ASC OFFSET 0 LIMIT 0)]]></ObfusctedQuery>
|
||||||
</Output>
|
</Output>
|
||||||
</Result>
|
</Result>
|
||||||
</Results>
|
</Results>
|
Загрузка…
Ссылка в новой задаче