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:
leminh98 2024-04-01 12:18:15 -07:00 коммит произвёл GitHub
Родитель 578832669c
Коммит 15d83a7d08
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
11 изменённых файлов: 1788 добавлений и 964 удалений

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

@ -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>
@ -449,10 +498,14 @@ namespace Microsoft.Azure.Cosmos.Linq
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:
// 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:
// 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
// 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()

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@ -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>