diff --git a/src/Microsoft.OData.Core/UriParser/Aggregation/ApplyBinder.cs b/src/Microsoft.OData.Core/UriParser/Aggregation/ApplyBinder.cs index 1bae61ffe4..1485a999d3 100644 --- a/src/Microsoft.OData.Core/UriParser/Aggregation/ApplyBinder.cs +++ b/src/Microsoft.OData.Core/UriParser/Aggregation/ApplyBinder.cs @@ -61,6 +61,7 @@ public ApplyClause BindApply(IEnumerable tokens) case QueryTokenKind.Compute: var compute = BindComputeToken((ComputeToken)token); transformations.Add(compute); + state.AggregatedPropertyNames = compute.Expressions.Select(statement => statement.Alias).ToList(); break; case QueryTokenKind.Expand: SelectExpandClause expandClause = SelectExpandSemanticBinder.Bind(this.odataPathInfo, (ExpandToken)token, null, this.configuration); diff --git a/src/Microsoft.OData.Core/UriParser/Aggregation/ApplyClause.cs b/src/Microsoft.OData.Core/UriParser/Aggregation/ApplyClause.cs index a02aa44355..b2dfc7b603 100644 --- a/src/Microsoft.OData.Core/UriParser/Aggregation/ApplyClause.cs +++ b/src/Microsoft.OData.Core/UriParser/Aggregation/ApplyClause.cs @@ -21,6 +21,8 @@ public sealed class ApplyClause private readonly IEnumerable lastGroupByPropertyNodes; + private readonly List lastComputeExpressions; + /// /// Create a ApplyClause. /// @@ -51,6 +53,11 @@ public ApplyClause(IList transformations) break; } + else if (transformations[i].Kind == TransformationNodeKind.Compute) + { + lastComputeExpressions = lastComputeExpressions ?? new List(); + lastComputeExpressions.AddRange((transformations[i] as ComputeTransformationNode).Expressions); + } } } @@ -73,12 +80,21 @@ internal string GetContextUri() internal List GetLastAggregatedPropertyNames() { - if (lastAggregateExpressions != null) + if (lastAggregateExpressions == null && lastComputeExpressions == null) { - return lastAggregateExpressions.Select(statement => statement.Alias).ToList(); + return null; } - return null; + List result = new List(); + if (lastAggregateExpressions != null) + { + result.AddRange(lastAggregateExpressions.Select(statement => statement.Alias)); + } + if (lastComputeExpressions != null) + { + result.AddRange(lastComputeExpressions.Select(statement => statement.Alias)); + } + return result; } private string CreatePropertiesUriSegment( diff --git a/src/Microsoft.OData.Core/UriParser/Parsers/UriQueryExpressionParser.cs b/src/Microsoft.OData.Core/UriParser/Parsers/UriQueryExpressionParser.cs index caa04439a1..217410ad4b 100644 --- a/src/Microsoft.OData.Core/UriParser/Parsers/UriQueryExpressionParser.cs +++ b/src/Microsoft.OData.Core/UriParser/Parsers/UriQueryExpressionParser.cs @@ -30,7 +30,7 @@ public sealed class UriQueryExpressionParser /// /// List of supported $apply keywords /// - private static readonly string supportedKeywords = string.Join("|", new string[] { ExpressionConstants.KeywordAggregate, ExpressionConstants.KeywordFilter, ExpressionConstants.KeywordGroupBy }); + private static readonly string supportedKeywords = string.Join("|", new string[] { ExpressionConstants.KeywordAggregate, ExpressionConstants.KeywordFilter, ExpressionConstants.KeywordGroupBy, ExpressionConstants.KeywordCompute, ExpressionConstants.KeywordExpand }); /// /// Set of parsed parameters diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/ExapndOptionFunctionalTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/ExapndOptionFunctionalTests.cs index 82a00b158f..9382b0fa6f 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/ExapndOptionFunctionalTests.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/ExapndOptionFunctionalTests.cs @@ -255,7 +255,7 @@ public void ApplyWithInvalidExpression() { Action action = () => this.Run("MyFriendsDogs($apply=Invalid Expression)", PersonType, PeopleSet); var exception = Assert.Throws(action); - Assert.Equal("'aggregate|filter|groupby' expected at position 0 in 'Invalid Expression'.", exception.Message); + Assert.Equal("'aggregate|filter|groupby|compute|expand' expected at position 0 in 'Invalid Expression'.", exception.Message); } #endregion $apply diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/FilterAndOrderByFunctionalTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/FilterAndOrderByFunctionalTests.cs index 27a6a52ffa..86ce2538c0 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/FilterAndOrderByFunctionalTests.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/FilterAndOrderByFunctionalTests.cs @@ -1144,6 +1144,22 @@ public void AggregatedPropertyTreatedAsOpenPropertyInOrderBy() orderByClause.Expression.ShouldBeSingleValueOpenPropertyAccessQueryNode("Total"); } + [Fact] + public void ComputedPropertyTreatedAsOpenPropertyInOrderBy() + { + var odataQueryOptionParser = new ODataQueryOptionParser(HardCodedTestModel.TestModel, + HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet(), + new Dictionary() + { + {"$orderby", "DoubleTotal asc"}, + {"$apply", "aggregate(FavoriteNumber with sum as Total)/compute(Total mul 2 as DoubleTotal)"} + }); + odataQueryOptionParser.ParseApply(); + var orderByClause = odataQueryOptionParser.ParseOrderBy(); + orderByClause.Direction.Should().Be(OrderByDirection.Ascending); + orderByClause.Expression.ShouldBeSingleValueOpenPropertyAccessQueryNode("DoubleTotal"); + } + [Fact] public void AggregatedPropertiesTreatedAsOpenPropertyInOrderBy() { @@ -1163,6 +1179,58 @@ public void AggregatedPropertiesTreatedAsOpenPropertyInOrderBy() orderByClause.Expression.ShouldBeSingleValueOpenPropertyAccessQueryNode("Max"); } + [Fact] + public void AggregatedAndComputePropertiesTreatedAsOpenPropertyInOrderBy() + { + var odataQueryOptionParser = new ODataQueryOptionParser(HardCodedTestModel.TestModel, + HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet(), + new Dictionary() + { + {"$orderby", "DoubleTotal asc, Total desc"}, + {"$apply", "aggregate(FavoriteNumber with sum as Total)/compute(Total mul 2 as DoubleTotal)"} + }); + odataQueryOptionParser.ParseApply(); + var orderByClause = odataQueryOptionParser.ParseOrderBy(); + orderByClause.Direction.Should().Be(OrderByDirection.Ascending); + orderByClause.Expression.ShouldBeSingleValueOpenPropertyAccessQueryNode("DoubleTotal"); + orderByClause = orderByClause.ThenBy; + orderByClause.Direction.Should().Be(OrderByDirection.Descending); + orderByClause.Expression.ShouldBeSingleValueOpenPropertyAccessQueryNode("Total"); + } + + [Fact] + public void MultipleComputePropertiesTreatedAsOpenPropertyInOrderBy() + { + var odataQueryOptionParser = new ODataQueryOptionParser(HardCodedTestModel.TestModel, + HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet(), + new Dictionary() + { + {"$orderby", "DoubleTotal1 asc, DoubleTotal2 desc"}, + {"$apply", "aggregate(FavoriteNumber with sum as Total)/compute(Total mul 2 as DoubleTotal1)/compute(DoubleTotal1 mul 2 as DoubleTotal2)"} + }); + odataQueryOptionParser.ParseApply(); + var orderByClause = odataQueryOptionParser.ParseOrderBy(); + orderByClause.Direction.Should().Be(OrderByDirection.Ascending); + orderByClause.Expression.ShouldBeSingleValueOpenPropertyAccessQueryNode("DoubleTotal1"); + orderByClause = orderByClause.ThenBy; + orderByClause.Direction.Should().Be(OrderByDirection.Descending); + orderByClause.Expression.ShouldBeSingleValueOpenPropertyAccessQueryNode("DoubleTotal2"); + } + + [Fact] + public void ReferenceComputeAliasCreatedBeforeAggrageteThrows() + { + var odataQueryOptionParser = new ODataQueryOptionParser(HardCodedTestModel.TestModel, + HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet(), + new Dictionary() + { + {"$orderby", "DoubleFavorite asc"}, + {"$apply", "compute(FavoriteNumber mul 2 as DoubleFavorite)/aggregate(DoubleFavorite with sum as Total)"} + }); + Action parseAction = () => { odataQueryOptionParser.ParseApply(); odataQueryOptionParser.ParseOrderBy(); }; + parseAction.ShouldThrow().WithMessage(ODataErrorStrings.MetadataBinder_PropertyNotDeclared("Fully.Qualified.Namespace.Person", "DoubleFavorite")); + } + [Fact] public void ActionsThrowOnClosedInOrderby() { diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/UriParser/Extensions/Binders/ApplyBinderTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/UriParser/Extensions/Binders/ApplyBinderTests.cs index b102ea37b9..5abcf29cc3 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/UriParser/Extensions/Binders/ApplyBinderTests.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/UriParser/Extensions/Binders/ApplyBinderTests.cs @@ -247,6 +247,30 @@ public void BindApplyWitMultipleTokensShouldReturnApplyClause() scecondAggregate.Should().NotBeNull(); } + [Fact] + public void BindApplyWithComputeShouldReturnApplyClause() + { + var tokens = _parser.ParseApply("compute(UnitPrice mul 5 as BigPrice)"); + + var binder = new ApplyBinder(FakeBindMethods.BindSingleComplexProperty, _bindingState); + var actual = binder.BindApply(tokens); + + actual.Should().NotBeNull(); + actual.Transformations.Should().HaveCount(1); + + var transformations = actual.Transformations.ToList(); + var compute = transformations[0] as ComputeTransformationNode; + + compute.Should().NotBeNull(); + compute.Kind.Should().Be(TransformationNodeKind.Compute); + compute.Expressions.Should().HaveCount(1); + + var statements = compute.Expressions.ToList(); + var statement = statements[0]; + VerifyIsFakeSingleValueNode(statement.Expression); + statement.Alias.ShouldBeEquivalentTo("BigPrice"); + } + [Fact] public void BindApplyWithEntitySetAggregationReturnApplyClause() { diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/UriParser/Parsers/UriQueryExpressionParserTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/UriParser/Parsers/UriQueryExpressionParserTests.cs index 120a47abd8..376d1b98bb 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/UriParser/Parsers/UriQueryExpressionParserTests.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/UriParser/Parsers/UriQueryExpressionParserTests.cs @@ -130,7 +130,7 @@ public void ParseApplyWithInvalidTransformationIdentifierShouldThrow() { string apply = "invalid(UnitPrice with sum as TotalPrice)"; Action parse = () => this.testSubject.ParseApply(apply); - parse.ShouldThrow().Where(e => e.Message == ErrorStrings.UriQueryExpressionParser_KeywordOrIdentifierExpected("aggregate|filter|groupby",0,apply)); + parse.ShouldThrow().Where(e => e.Message == ErrorStrings.UriQueryExpressionParser_KeywordOrIdentifierExpected("aggregate|filter|groupby|compute|expand", 0,apply)); } [Fact] @@ -407,7 +407,7 @@ public void ParseApplyWithGroupByAndAggregateMissingCloseParenShouldThrow() { string apply = "groupBy((UnitPrice), aggregate(UnitPrice with sum as TotalPrice)"; Action parse = () => this.testSubject.ParseApply(apply); - parse.ShouldThrow().Where(e => e.Message == ErrorStrings.UriQueryExpressionParser_KeywordOrIdentifierExpected("aggregate|filter|groupby", 0, apply)); + parse.ShouldThrow().Where(e => e.Message == ErrorStrings.UriQueryExpressionParser_KeywordOrIdentifierExpected("aggregate|filter|groupby|compute|expand", 0, apply)); } [Fact] @@ -653,6 +653,30 @@ public void ParseApplyWithNestedFunctionAggregation() funcToken.Name.ShouldBeEquivalentTo("cast"); } + [Fact] + public void ParseApplyWithComputeWithMathematicalOperations() + { + string compute = "compute(Prop1 mul Prop2 as Product,Prop1 div Prop2 as Ratio,Prop2 mod Prop2 as Remainder)"; + ComputeToken token = this.testSubject.ParseApply(compute).First() as ComputeToken; + token.Kind.ShouldBeEquivalentTo(QueryTokenKind.Compute); + List tokens = token.Expressions.ToList(); + tokens.Count.Should().Be(3); + tokens[0].Kind.ShouldBeEquivalentTo(QueryTokenKind.ComputeExpression); + tokens[1].Kind.ShouldBeEquivalentTo(QueryTokenKind.ComputeExpression); + tokens[2].Kind.ShouldBeEquivalentTo(QueryTokenKind.ComputeExpression); + tokens[0].Alias.ShouldBeEquivalentTo("Product"); + tokens[1].Alias.ShouldBeEquivalentTo("Ratio"); + tokens[2].Alias.ShouldBeEquivalentTo("Remainder"); + (tokens[0].Expression as BinaryOperatorToken).OperatorKind.ShouldBeEquivalentTo(BinaryOperatorKind.Multiply); + (tokens[1].Expression as BinaryOperatorToken).OperatorKind.ShouldBeEquivalentTo(BinaryOperatorKind.Divide); + (tokens[2].Expression as BinaryOperatorToken).OperatorKind.ShouldBeEquivalentTo(BinaryOperatorKind.Modulo); + + Action accept1 = () => tokens[0].Accept(null); + accept1.ShouldThrow(); + Action accept2 = () => token.Accept(null); + accept2.ShouldThrow(); + } + [Fact] public void ParseComputeWithMathematicalOperations() {