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.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Data.Common;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Azure.Cosmos.CosmosElements;
|
||||
using Microsoft.Azure.Cosmos.Serialization.HybridRow;
|
||||
using Microsoft.Azure.Cosmos.Serializer;
|
||||
using Microsoft.Azure.Cosmos.Spatial;
|
||||
using Microsoft.Azure.Cosmos.SqlObjects;
|
||||
|
@ -64,6 +67,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
public const string FirstOrDefault = "FirstOrDefault";
|
||||
public const string Max = "Max";
|
||||
public const string Min = "Min";
|
||||
public const string GroupBy = "GroupBy";
|
||||
public const string OrderBy = "OrderBy";
|
||||
public const string OrderByDescending = "OrderByDescending";
|
||||
public const string Select = "Select";
|
||||
|
@ -109,7 +113,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// <param name="inputExpression">Expression to translate.</param>
|
||||
/// <param name="context">Context for translation.</param>
|
||||
|
@ -805,8 +809,8 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
|
||||
if (usePropertyRef)
|
||||
{
|
||||
SqlIdentifier propertyIdnetifier = SqlIdentifier.Create(memberName);
|
||||
SqlPropertyRefScalarExpression propertyRefExpression = SqlPropertyRefScalarExpression.Create(memberExpression, propertyIdnetifier);
|
||||
SqlIdentifier propertyIdentifier = SqlIdentifier.Create(memberName);
|
||||
SqlPropertyRefScalarExpression propertyRefExpression = SqlPropertyRefScalarExpression.Create(memberExpression, propertyIdentifier);
|
||||
return propertyRefExpression;
|
||||
}
|
||||
else
|
||||
|
@ -997,7 +1001,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
SqlQuery query = context.CurrentQuery.FlattenAsPossible().GetSqlQuery();
|
||||
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);
|
||||
|
||||
context.CurrentQuery = new QueryUnderConstruction(context.GetGenFreshParameterFunc());
|
||||
|
@ -1111,7 +1115,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
|
||||
Collection collection = ExpressionToSql.ConvertToCollection(body);
|
||||
context.PushCollection(collection);
|
||||
ParameterExpression parameter = context.GenFreshParameter(type, parameterName);
|
||||
ParameterExpression parameter = context.GenerateFreshParameter(type, parameterName);
|
||||
context.PushParameter(parameter, context.CurrentSubqueryBinding.ShouldBeOnNewQuery);
|
||||
context.PopParameter();
|
||||
context.PopCollection();
|
||||
|
@ -1120,7 +1124,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
}
|
||||
|
||||
/// <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.
|
||||
/// These methods are static extension methods of IQueryable or IEnumerable.
|
||||
/// </summary>
|
||||
|
@ -1149,11 +1153,18 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
|
||||
Type inputElementType = TypeSystem.GetElementType(inputCollection.Type);
|
||||
Collection collection = ExpressionToSql.Translate(inputCollection, context);
|
||||
|
||||
context.PushCollection(collection);
|
||||
|
||||
Collection result = new Collection(inputExpression.Method.Name);
|
||||
bool shouldBeOnNewQuery = context.CurrentQuery.ShouldBeOnNewQuery(inputExpression.Method.Name, inputExpression.Arguments.Count);
|
||||
context.PushSubqueryBinding(shouldBeOnNewQuery);
|
||||
|
||||
if (context.LastExpressionIsGroupBy)
|
||||
{
|
||||
throw new DocumentQueryException(string.Format(CultureInfo.CurrentCulture, "Group By cannot be followed by other methods"));
|
||||
}
|
||||
|
||||
switch (inputExpression.Method.Name)
|
||||
{
|
||||
case LinqMethods.Any:
|
||||
|
@ -1219,6 +1230,13 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
context.CurrentQuery = context.CurrentQuery.AddSelectClause(select, context);
|
||||
break;
|
||||
}
|
||||
case LinqMethods.GroupBy:
|
||||
{
|
||||
context.CurrentQuery = context.PackageCurrentQueryIfNeccessary();
|
||||
result = ExpressionToSql.VisitGroupBy(returnElementType, inputExpression.Arguments, context);
|
||||
context.LastExpressionIsGroupBy = true;
|
||||
break;
|
||||
}
|
||||
case LinqMethods.OrderBy:
|
||||
{
|
||||
SqlOrderByClause orderBy = ExpressionToSql.VisitOrderBy(inputExpression.Arguments, false, context);
|
||||
|
@ -1376,6 +1394,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
case LinqMethods.Skip:
|
||||
case LinqMethods.Take:
|
||||
case LinqMethods.Distinct:
|
||||
case LinqMethods.GroupBy:
|
||||
isSubqueryExpression = true;
|
||||
expressionObjKind = SubqueryKind.ArrayScalarExpression;
|
||||
break;
|
||||
|
@ -1405,7 +1424,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
}
|
||||
|
||||
/// <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,
|
||||
/// 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
|
||||
|
@ -1508,7 +1527,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
{
|
||||
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(
|
||||
query,
|
||||
isMinMaxAvgMethod ? SubqueryKind.ArrayScalarExpression : expressionObjKind.Value);
|
||||
|
@ -1585,7 +1604,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
|
||||
QueryUnderConstruction queryBeforeVisit = 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;
|
||||
|
||||
if (shouldBeOnNewQuery) context.CurrentSubqueryBinding.ShouldBeOnNewQuery = false;
|
||||
|
@ -1663,9 +1682,108 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
Binding binding;
|
||||
SqlQuery query = ExpressionToSql.CreateSubquery(lambda.Body, lambda.Parameters, context);
|
||||
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);
|
||||
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;
|
||||
|
@ -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.
|
||||
// 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.
|
||||
ParameterExpression parameter = context.GenFreshParameter(typeof(object), ExpressionToSql.DefaultParameterName);
|
||||
ParameterExpression parameter = context.GenerateFreshParameter(typeof(object), ExpressionToSql.DefaultParameterName);
|
||||
context.PushParameter(parameter, context.CurrentSubqueryBinding.ShouldBeOnNewQuery);
|
||||
context.PopParameter();
|
||||
|
||||
|
@ -1848,16 +1966,21 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
SqlScalarExpression aggregateExpression;
|
||||
if (arguments.Count == 1)
|
||||
{
|
||||
// Need to trigger parameter binding for cases where a aggregate function immediately follows a member access.
|
||||
ParameterExpression parameter = context.GenFreshParameter(typeof(object), ExpressionToSql.DefaultParameterName);
|
||||
// Need to trigger parameter binding for cases where an aggregate function immediately follows a member access.
|
||||
ParameterExpression parameter = context.GenerateFreshParameter(typeof(object), ExpressionToSql.DefaultParameterName);
|
||||
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);
|
||||
context.PopParameter();
|
||||
}
|
||||
else if (arguments.Count == 2)
|
||||
{
|
||||
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
|
||||
{
|
||||
|
@ -1884,7 +2007,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
|
||||
// We consider Distinct as Distinct(v0 => v0)
|
||||
// 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);
|
||||
SqlScalarExpression sqlfunc = ExpressionToSql.VisitNonSubqueryScalarLambda(identityLambda, context);
|
||||
SqlSelectSpec sqlSpec = SqlSelectValueSpec.Create(sqlfunc);
|
||||
|
|
|
@ -27,7 +27,16 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
/// <summary>
|
||||
/// Binding for the FROM parameters.
|
||||
/// </summary>
|
||||
public FromParameterBindings fromParameters
|
||||
public FromParameterBindings FromParameters
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binding for the Group By clause.
|
||||
/// </summary>
|
||||
public FromParameterBindings GroupByParameter
|
||||
{
|
||||
get;
|
||||
set;
|
||||
|
@ -51,6 +60,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
private SqlSelectClause selectClause;
|
||||
private SqlWhereClause whereClause;
|
||||
private SqlOrderByClause orderByClause;
|
||||
private SqlGroupByClause groupByClause;
|
||||
|
||||
// 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.
|
||||
|
@ -61,7 +71,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
private Lazy<ParameterExpression> alias;
|
||||
|
||||
/// <summary>
|
||||
/// Input subquery.
|
||||
/// Input subquery / query to the left of the current query.
|
||||
/// </summary>
|
||||
private QueryUnderConstruction inputQuery;
|
||||
|
||||
|
@ -72,7 +82,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
|
||||
public QueryUnderConstruction(Func<string, ParameterExpression> aliasCreatorFunc, QueryUnderConstruction inputQuery)
|
||||
{
|
||||
this.fromParameters = new FromParameterBindings();
|
||||
this.FromParameters = new FromParameterBindings();
|
||||
this.aliasCreatorFunc = aliasCreatorFunc;
|
||||
this.inputQuery = inputQuery;
|
||||
this.alias = new Lazy<ParameterExpression>(() => aliasCreatorFunc(QueryUnderConstruction.DefaultSubqueryRoot));
|
||||
|
@ -85,22 +95,22 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
|
||||
public void AddBinding(Binding binding)
|
||||
{
|
||||
this.fromParameters.Add(binding);
|
||||
this.FromParameters.Add(binding);
|
||||
}
|
||||
|
||||
public ParameterExpression GetInputParameterInContext(bool isInNewQuery)
|
||||
{
|
||||
return isInNewQuery ? this.Alias : this.fromParameters.GetInputParameter();
|
||||
return isInNewQuery ? this.Alias : this.FromParameters.GetInputParameter();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a FROM clause from a set of FROM parameter bindings.
|
||||
/// </summary>
|
||||
/// <returns>The created FROM clause.</returns>
|
||||
private SqlFromClause CreateFrom(SqlCollectionExpression inputCollectionExpression)
|
||||
private SqlFromClause CreateFromClause(SqlCollectionExpression inputCollectionExpression)
|
||||
{
|
||||
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,
|
||||
// which is the input paramter name, should be omitted.
|
||||
|
@ -147,7 +157,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
ParameterExpression inputParam = this.inputQuery.Alias;
|
||||
SqlIdentifier identifier = SqlIdentifier.Create(inputParam.Name);
|
||||
SqlAliasedCollectionExpression colExp = SqlAliasedCollectionExpression.Create(collection, identifier);
|
||||
SqlFromClause fromClause = this.CreateFrom(colExp);
|
||||
SqlFromClause fromClause = this.CreateFromClause(colExp);
|
||||
return fromClause;
|
||||
}
|
||||
|
||||
|
@ -169,7 +179,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
}
|
||||
else
|
||||
{
|
||||
fromClause = this.CreateFrom(inputCollectionExpression: null);
|
||||
fromClause = this.CreateFromClause(inputCollectionExpression: null);
|
||||
}
|
||||
|
||||
// Create a SqlSelectClause with the topSpec.
|
||||
|
@ -178,7 +188,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
SqlSelectClause selectClause = this.selectClause;
|
||||
if (selectClause == null)
|
||||
{
|
||||
string parameterName = this.fromParameters.GetInputParameter().Name;
|
||||
string parameterName = this.FromParameters.GetInputParameter().Name;
|
||||
SqlScalarExpression parameterExpression = SqlPropertyRefScalarExpression.Create(null, SqlIdentifier.Create(parameterName));
|
||||
selectClause = this.selectClause = SqlSelectClause.Create(SqlSelectValueSpec.Create(parameterExpression));
|
||||
}
|
||||
|
@ -186,7 +196,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
SqlOffsetLimitClause offsetLimitClause = (this.offsetSpec != null) ?
|
||||
SqlOffsetLimitClause.Create(this.offsetSpec, this.limitSpec ?? SqlLimitSpec.Create(SqlNumberLiteral.Create(int.MaxValue))) :
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -198,7 +208,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
public QueryUnderConstruction PackageQuery(HashSet<ParameterExpression> inScope)
|
||||
{
|
||||
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;
|
||||
return result;
|
||||
}
|
||||
|
@ -214,13 +224,14 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
// 1. Select clause appears after Distinct
|
||||
// 2. There are any operations after Take that is not a pure Select.
|
||||
// 3. There are nested Select, Where or OrderBy
|
||||
// 4. Group by clause appears after Select
|
||||
QueryUnderConstruction parentQuery = null;
|
||||
QueryUnderConstruction flattenQuery = null;
|
||||
bool seenSelect = false;
|
||||
bool seenAnyNonSelectOp = false;
|
||||
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))
|
||||
{
|
||||
|
@ -232,8 +243,15 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
// In Select -> SelectMany cases, fromParameter substitution is not yet supported .
|
||||
// Therefore these are un-flattenable.
|
||||
if (query.inputQuery != null &&
|
||||
(query.fromParameters.GetBindings().First().Parameter.Name == query.inputQuery.Alias.Name) &&
|
||||
query.fromParameters.GetBindings().Any(b => b.ParameterDefinition != null))
|
||||
(query.FromParameters.GetBindings().First().Parameter.Name == query.inputQuery.Alias.Name) &&
|
||||
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;
|
||||
break;
|
||||
|
@ -253,10 +271,12 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
seenAnyNonSelectOp |=
|
||||
(query.whereClause != null) ||
|
||||
(query.orderByClause != null) ||
|
||||
(query.groupByClause != null) ||
|
||||
(query.topSpec != null) ||
|
||||
(query.offsetSpec != null) ||
|
||||
query.fromParameters.GetBindings().Any(b => b.ParameterDefinition != null) ||
|
||||
((query.selectClause != null) && (query.selectClause.HasDistinct || this.HasSelectAggregate()));
|
||||
query.FromParameters.GetBindings().Any(b => b.ParameterDefinition != null) ||
|
||||
((query.selectClause != null) && (query.selectClause.HasDistinct ||
|
||||
this.HasSelectAggregate()));
|
||||
parentQuery = query;
|
||||
}
|
||||
|
||||
|
@ -272,7 +292,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
private QueryUnderConstruction Flatten()
|
||||
{
|
||||
// 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
|
||||
// SELECT fo(fi(x)) FROM x WHERE gi(x) AND (go(fi(x))
|
||||
if (this.inputQuery == null)
|
||||
|
@ -281,7 +301,8 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
if (this.selectClause == null)
|
||||
{
|
||||
// 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));
|
||||
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.
|
||||
string paramName = null;
|
||||
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);
|
||||
}
|
||||
|
||||
foreach (Binding binding in this.fromParameters.GetBindings())
|
||||
foreach (Binding binding in this.FromParameters.GetBindings())
|
||||
{
|
||||
if (binding.ParameterDefinition == null || inputQueryParams.Contains(binding.Parameter.Name))
|
||||
{
|
||||
|
@ -316,11 +337,14 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
}
|
||||
|
||||
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);
|
||||
SqlOrderByClause composedOrderBy = this.Substitute(inputSelect.SelectSpec, replacement, this.orderByClause);
|
||||
SqlGroupByClause composedGroupBy = this.Substitute(inputSelect.SelectSpec, replacement, this.groupByClause);
|
||||
SqlWhereClause and = QueryUnderConstruction.CombineWithConjunction(inputwhere, composedWhere);
|
||||
FromParameterBindings fromParams = QueryUnderConstruction.CombineInputParameters(flatInput.fromParameters, this.fromParameters);
|
||||
FromParameterBindings fromParams = QueryUnderConstruction.CombineInputParameters(flatInput.FromParameters, this.FromParameters);
|
||||
SqlOffsetSpec offsetSpec;
|
||||
SqlLimitSpec limitSpec;
|
||||
if (flatInput.offsetSpec != null)
|
||||
|
@ -338,8 +362,9 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
selectClause = composedSelect,
|
||||
whereClause = and,
|
||||
inputQuery = null,
|
||||
fromParameters = flatInput.fromParameters,
|
||||
FromParameters = flatInput.FromParameters,
|
||||
orderByClause = composedOrderBy ?? this.inputQuery.orderByClause,
|
||||
groupByClause = composedGroupBy ?? this.inputQuery.groupByClause,
|
||||
offsetSpec = offsetSpec,
|
||||
limitSpec = limitSpec,
|
||||
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)
|
||||
{
|
||||
SqlSelectSpec selectSpec = inputSelectClause.SelectSpec;
|
||||
SqlSelectSpec inputSelectSpec = inputSelectClause.SelectSpec;
|
||||
|
||||
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)
|
||||
{
|
||||
SqlSelectSpec intoSpec = selectClause.SelectSpec;
|
||||
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;
|
||||
|
@ -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: " + selectSpec.GetType());
|
||||
throw new DocumentQueryException("Unexpected SQL select clause type: " + inputSelectSpec.GetType());
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
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>
|
||||
/// Determine if the current method call should create a new QueryUnderConstruction node or not.
|
||||
/// </summary>
|
||||
|
@ -450,9 +499,13 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
{
|
||||
// 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
|
||||
// | |
|
||||
// |-> Group By->|
|
||||
//
|
||||
// The order for the corresponding LINQ operations is:
|
||||
// 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
|
||||
// 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;
|
||||
|
||||
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.
|
||||
case LinqMethods.Any:
|
||||
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
|
||||
shouldPackage = (this.topSpec != 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;
|
||||
|
||||
case LinqMethods.Skip:
|
||||
|
@ -592,6 +654,16 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
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)
|
||||
{
|
||||
QueryUnderConstruction result = context.PackageCurrentQueryIfNeccessary();
|
||||
|
@ -826,6 +898,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
private bool HasSelectAggregate()
|
||||
{
|
||||
string functionCallName = ((this.selectClause?.SelectSpec as SqlSelectValueSpec)?.Expression as SqlFunctionCallScalarExpression)?.Name.Value;
|
||||
|
||||
return (functionCallName != null) &&
|
||||
((functionCallName == SqlFunctionCallScalarExpression.Names.Max) ||
|
||||
(functionCallName == SqlFunctionCallScalarExpression.Names.Min) ||
|
||||
|
|
|
@ -43,6 +43,16 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
/// </summary>
|
||||
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>
|
||||
/// 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.
|
||||
|
@ -86,6 +96,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
this.subqueryBindingStack = new Stack<SubqueryBinding>();
|
||||
this.Parameters = parameters;
|
||||
this.clientOperation = null;
|
||||
this.LastExpressionIsGroupBy = false;
|
||||
|
||||
if (linqSerializerOptionsInternal?.CustomCosmosLinqSerializer != null)
|
||||
{
|
||||
|
@ -104,6 +115,8 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
this.CosmosLinqSerializer = TranslationContext.DefaultLinqSerializer;
|
||||
this.MemberNames = TranslationContext.DefaultMemberNames;
|
||||
}
|
||||
|
||||
this.GroupByKeySubstitution = new ParameterSubstitution();
|
||||
}
|
||||
|
||||
public ScalarOperationKind ClientOperation => this.clientOperation ?? ScalarOperationKind.None;
|
||||
|
@ -120,17 +133,25 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
|
||||
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);
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
return (paramName) => this.GenFreshParameter(typeof(object), paramName);
|
||||
return (paramName) => this.GenerateFreshParameter(typeof(object), paramName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -211,12 +232,12 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
throw new ArgumentNullException("collection");
|
||||
}
|
||||
|
||||
this.collectionStack.Add(collection);
|
||||
if (this.CurrentQuery.GroupByParameter == null) this.collectionStack.Add(collection);
|
||||
}
|
||||
|
||||
public void PopCollection()
|
||||
{
|
||||
this.collectionStack.RemoveAt(this.collectionStack.Count - 1);
|
||||
if (this.CurrentQuery.GroupByParameter == null) this.collectionStack.RemoveAt(this.collectionStack.Count - 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -226,7 +247,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
/// <param name="name">Suggested name for the input parameter.</param>
|
||||
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>
|
||||
|
@ -237,7 +258,7 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
public void SetFromParameter(ParameterExpression parameter, SqlCollection collection)
|
||||
{
|
||||
Binding binding = new Binding(parameter, collection, isInCollection: true);
|
||||
this.CurrentQuery.fromParameters.Add(binding);
|
||||
this.CurrentQuery.FromParameters.Add(binding);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -45,15 +45,16 @@ namespace Microsoft.Azure.Cosmos.Linq
|
|||
/// <param name="prefix">Prefix for the parameter name.</param>
|
||||
/// <param name="type">Parameter type.</param>
|
||||
/// <param name="inScope">Names to avoid.</param>
|
||||
/// <param name="includeSuffix">Enable suffix to parameter name</param>
|
||||
/// <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;
|
||||
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);
|
||||
if (!inScope.Any(p => p.Name.Equals(name)))
|
||||
if (!inScope.Any(p => p.Name.Equals(name)) || !includeSuffix)
|
||||
{
|
||||
inScope.Add(param);
|
||||
return param;
|
||||
|
|
|
@ -495,6 +495,7 @@ namespace Microsoft.Azure.Cosmos.SqlObjects.Visitors
|
|||
|
||||
if (sqlQuery.GroupByClause != null)
|
||||
{
|
||||
this.WriteDelimiter(string.Empty);
|
||||
sqlQuery.GroupByClause.Accept(this);
|
||||
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);
|
||||
}
|
||||
|
||||
[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]
|
||||
[Ignore]
|
||||
public void DebuggingTest()
|
||||
|
|
|
@ -744,6 +744,7 @@ namespace Microsoft.Azure.Cosmos.Services.Management.Tests
|
|||
{ "JOIN", "\nJOIN" },
|
||||
{ "ORDER BY", "\nORDER BY" },
|
||||
{ "OFFSET", "\nOFFSET" },
|
||||
{ "GROUP BY", "\nGROUP BY" },
|
||||
{ " )", "\n)" }
|
||||
};
|
||||
|
||||
|
@ -785,7 +786,6 @@ namespace Microsoft.Azure.Cosmos.Services.Management.Tests
|
|||
const string oneTab = " ";
|
||||
const string startCue = "SELECT";
|
||||
const string endCue = ")";
|
||||
|
||||
string[] tokens = sb.ToString().Split('\n');
|
||||
bool firstSelect = true;
|
||||
sb.Length = 0;
|
||||
|
|
|
@ -193,6 +193,9 @@
|
|||
<Content Include="BaselineTest\TestBaseline\LinqGeneralBaselineTests.ValidateLinqQueries.xml">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="BaselineTest\TestBaseline\LinqGeneralBaselineTests.TestGroupByTranslation.xml">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="BaselineTest\TestBaseline\LinqGeneralBaselineTests.TestOrderByTranslation.xml">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<Query><![CDATA[SELECT * GROUP BY 1]]></Query>
|
||||
</Input>
|
||||
<Output>
|
||||
<ParsedQuery><![CDATA[SELECT *GROUP BY 1 ]]></ParsedQuery>
|
||||
<ParsedQuery><![CDATA[SELECT * GROUP BY 1 ]]></ParsedQuery>
|
||||
</Output>
|
||||
</Result>
|
||||
<Result>
|
||||
|
@ -14,7 +14,7 @@
|
|||
<Query><![CDATA[SELECT * GrOuP By 1]]></Query>
|
||||
</Input>
|
||||
<Output>
|
||||
<ParsedQuery><![CDATA[SELECT *GROUP BY 1 ]]></ParsedQuery>
|
||||
<ParsedQuery><![CDATA[SELECT * GROUP BY 1 ]]></ParsedQuery>
|
||||
</Output>
|
||||
</Result>
|
||||
<Result>
|
||||
|
@ -23,7 +23,7 @@
|
|||
<Query><![CDATA[SELECT * GROUP BY 1, 2, 3]]></Query>
|
||||
</Input>
|
||||
<Output>
|
||||
<ParsedQuery><![CDATA[SELECT *GROUP BY 1, 2, 3 ]]></ParsedQuery>
|
||||
<ParsedQuery><![CDATA[SELECT * GROUP BY 1, 2, 3 ]]></ParsedQuery>
|
||||
</Output>
|
||||
</Result>
|
||||
<Result>
|
||||
|
|
|
@ -989,16 +989,17 @@ OFFSET 0 LIMIT 0
|
|||
}]]></SqlObject>
|
||||
</Input>
|
||||
<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[
|
||||
SELECT *
|
||||
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
|
||||
OFFSET 0 LIMIT 0
|
||||
]]></PrettyPrint>
|
||||
<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>
|
||||
</Result>
|
||||
<Result>
|
||||
|
@ -1127,18 +1128,19 @@ OFFSET 0 LIMIT 0
|
|||
}]]></SqlObject>
|
||||
</Input>
|
||||
<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[
|
||||
(
|
||||
SELECT *
|
||||
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
|
||||
OFFSET 0 LIMIT 0
|
||||
)
|
||||
]]></PrettyPrint>
|
||||
<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>
|
||||
</Result>
|
||||
<Result>
|
||||
|
@ -1267,18 +1269,19 @@ OFFSET 0 LIMIT 0
|
|||
}]]></SqlObject>
|
||||
</Input>
|
||||
<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[
|
||||
ARRAY(
|
||||
SELECT *
|
||||
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
|
||||
OFFSET 0 LIMIT 0
|
||||
)
|
||||
]]></PrettyPrint>
|
||||
<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>
|
||||
</Result>
|
||||
<Result>
|
||||
|
@ -1407,18 +1410,19 @@ ARRAY(
|
|||
}]]></SqlObject>
|
||||
</Input>
|
||||
<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[
|
||||
EXISTS(
|
||||
SELECT *
|
||||
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
|
||||
OFFSET 0 LIMIT 0
|
||||
)
|
||||
]]></PrettyPrint>
|
||||
<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>
|
||||
</Result>
|
||||
</Results>
|
Загрузка…
Ссылка в новой задаче