Permalink
Browse files

Adding support and tests for custom aggregation methods.

  • Loading branch information...
athoscouto authored and robward-ms committed Feb 14, 2017
1 parent 0dc7067 commit d064a1c6358130c581bb9d5bb32e774f8e921992
@@ -609,7 +609,7 @@ internal sealed class TextRes {
internal const string UriQueryExpressionParser_RangeVariableAlreadyDeclared = "UriQueryExpressionParser_RangeVariableAlreadyDeclared";
internal const string UriQueryExpressionParser_AsExpected = "UriQueryExpressionParser_AsExpected";
internal const string UriQueryExpressionParser_WithExpected = "UriQueryExpressionParser_WithExpected";
internal const string UriQueryExpressionParser_UnrecognizedWithVerb = "UriQueryExpressionParser_UnrecognizedWithVerb";
internal const string UriQueryExpressionParser_UnrecognizedWithMethod = "UriQueryExpressionParser_UnrecognizedWithMethod";
internal const string UriQueryExpressionParser_PropertyPathExpected = "UriQueryExpressionParser_PropertyPathExpected";
internal const string UriQueryExpressionParser_KeywordOrIdentifierExpected = "UriQueryExpressionParser_KeywordOrIdentifierExpected";
internal const string UriQueryPathParser_RequestUriDoesNotHaveTheCorrectBaseUri = "UriQueryPathParser_RequestUriDoesNotHaveTheCorrectBaseUri";
@@ -661,7 +661,7 @@ UriQueryExpressionParser_CannotCreateStarTokenFromNonStar=Expecting a Star token
UriQueryExpressionParser_RangeVariableAlreadyDeclared=The range variable '{0}' has already been declared.
UriQueryExpressionParser_AsExpected='as' expected at position {0} in '{1}'.
UriQueryExpressionParser_WithExpected='with' expected at position {0} in '{1}'.
UriQueryExpressionParser_UnrecognizedWithVerb=Unrecognized with '{0}' at '{1}' in '{2}'.
UriQueryExpressionParser_UnrecognizedWithMethod=Unrecognized with '{0}' at '{1}' in '{2}'.
UriQueryExpressionParser_PropertyPathExpected=Expression expected at position {0} in '{1}'.
UriQueryExpressionParser_KeywordOrIdentifierExpected='{0}' expected at position {1} in '{2}'.
@@ -4154,8 +4154,8 @@ internal static class Strings {
/// <summary>
/// A string like "Unrecognized with '{0}' at '{1}' in '{2}'."
/// </summary>
internal static string UriQueryExpressionParser_UnrecognizedWithVerb(object p0, object p1, object p2) {
return Microsoft.OData.TextRes.GetString(Microsoft.OData.TextRes.UriQueryExpressionParser_UnrecognizedWithVerb, p0, p1, p2);
internal static string UriQueryExpressionParser_UnrecognizedWithMethod(object p0, object p1, object p2) {
return Microsoft.OData.TextRes.GetString(Microsoft.OData.TextRes.UriQueryExpressionParser_UnrecognizedWithMethod, p0, p1, p2);
}
/// <summary>
@@ -51,6 +51,9 @@ internal static class ExpressionConstants
/// <summary>',' constant to represent an value list separator.</summary>
internal const string SymbolComma = ",";
/// <summary>'.' constant to represent the value of a dot separator.</summary>
internal const string SymbolDot = ".";
/// <summary>'/' constant to represent the forward slash used in a query.</summary>
internal const string SymbolForwardSlash = "/";
@@ -16,6 +16,8 @@ public sealed class AggregateExpression
{
private readonly AggregationMethod method;
private readonly AggregationMethodDefinition methodDefinition;
private readonly SingleValueNode expression;
private readonly string alias;
@@ -41,6 +43,19 @@ public AggregateExpression(SingleValueNode expression, AggregationMethod method,
this.typeReference = typeReference;
}
/// <summary>
/// Create a AggregateExpression.
/// </summary>
/// <param name="expression">The aggregation expression.</param>
/// <param name="methodDefinition">The <see cref="AggregationMethodDefinition"/>.</param>
/// <param name="alias">The aggregation alias.</param>
/// <param name="typeReference">The <see cref="IEdmTypeReference"/> of this aggregate expression.</param>
public AggregateExpression(SingleValueNode expression, AggregationMethodDefinition methodDefinition, string alias, IEdmTypeReference typeReference)
: this(expression, methodDefinition.MethodKind, alias, typeReference)
{
this.methodDefinition = methodDefinition;
}
/// <summary>
/// Gets the aggregation expression.
/// </summary>
@@ -63,6 +78,17 @@ public AggregationMethod Method
}
}
/// <summary>
/// Gets the <see cref="AggregationMethodDefinition"/>.
/// </summary>
public AggregationMethodDefinition MethodDefinition
{
get
{
return methodDefinition;
}
}
/// <summary>
/// Gets the aggregation alias.
/// </summary>
@@ -20,18 +20,26 @@ internal sealed class AggregateExpressionToken : QueryToken
private readonly AggregationMethod method;
private readonly AggregationMethodDefinition methodDefinition;
private readonly string alias;
public AggregateExpressionToken(QueryToken expression, AggregationMethod withVerb, string alias)
public AggregateExpressionToken(QueryToken expression, AggregationMethod method, string alias)
{
ExceptionUtils.CheckArgumentNotNull(expression, "expression");
ExceptionUtils.CheckArgumentNotNull(alias, "alias");
this.expression = expression;
this.method = withVerb;
this.method = method;
this.alias = alias;
}
public AggregateExpressionToken(QueryToken expression, AggregationMethodDefinition methodDefinition, string alias)
: this(expression, methodDefinition.MethodKind, alias)
{
this.methodDefinition = methodDefinition;
}
public override QueryTokenKind Kind
{
get { return QueryTokenKind.AggregateExpression; }
@@ -42,6 +50,11 @@ public AggregationMethod Method
get { return this.method; }
}
public AggregationMethodDefinition MethodDefinition
{
get { return this.methodDefinition; }
}
public QueryToken Expression
{
get { return this.expression; }
@@ -4,42 +4,86 @@
// </copyright>
//---------------------------------------------------------------------
using System.Diagnostics;
namespace Microsoft.OData.UriParser.Aggregation
{
/// <summary>
/// Enumeration of methods used in the aggregation clause
/// </summary>
public enum AggregationMethod
{
/// <summary>
/// The aggregation method Sum.
/// </summary>
/// <summary>The aggregation method Sum.</summary>
Sum,
/// <summary>
/// The aggregation method Min.
/// </summary>
/// <summary>The aggregation method Min.</summary>
Min,
/// <summary>
/// The aggregation method Max.
/// </summary>
/// <summary>The aggregation method Max.</summary>
Max,
/// <summary>
/// The aggregation method Average.
/// </summary>
/// <summary>The aggregation method Average.</summary>
Average,
/// <summary>
/// The aggregation method CountDistinct.
/// </summary>
/// <summary>The aggregation method CountDistinct.</summary>
CountDistinct,
/// <summary>
/// The aggregation method Count.
/// Used only internally to represent the virtual property $count.
/// </summary>
/// <summary>The aggregation method Count. Used only internally to represent the virtual property $count.</summary>
VirtualPropertyCount,
/// <summary>A custom aggregation method.</summary>
Custom
}
/// <summary>
/// Class that encapsulates all the information needed to define a aggregation method.
/// </summary>
public sealed class AggregationMethodDefinition
{
/// <summary>Returns a definition for the sum aggregation method.</summary>
public static AggregationMethodDefinition Sum = new AggregationMethodDefinition(AggregationMethod.Sum);
/// <summary>Returns a definition for the min aggregation method.</summary>
public static AggregationMethodDefinition Min = new AggregationMethodDefinition(AggregationMethod.Min);
/// <summary>Returns a definition for the max aggregation method.</summary>
public static AggregationMethodDefinition Max = new AggregationMethodDefinition(AggregationMethod.Max);
/// <summary>Returns a definition for the average aggregation method.</summary>
public static AggregationMethodDefinition Average = new AggregationMethodDefinition(AggregationMethod.Average);
/// <summary>Returns a definition for the countdistinct aggregation method.</summary>
public static AggregationMethodDefinition CountDistinct = new AggregationMethodDefinition(AggregationMethod.CountDistinct);
/// <summary>Returns a definition for the aggregation method used to calculate $count.</summary>
public static AggregationMethodDefinition VirtualPropertyCount = new AggregationMethodDefinition(AggregationMethod.VirtualPropertyCount);
/// <summary>Private constructor. Instances should be aquired via static fields of via Custom method.</summary>
/// <param name="aggregationMethodType">The <see cref="AggregationMethod"/> of this method definition.</param>
private AggregationMethodDefinition(AggregationMethod aggregationMethodType)
{
this.MethodKind = aggregationMethodType;
}
/// <summary>Returns the <see cref="AggregationMethod"/> of this method definition.</summary>
public AggregationMethod MethodKind { get; private set; }
/// <summary>Returns the label of this method definition.</summary>
public string MethodLabel { get; private set; }
/// <summary>Creates a custom method definition from it's label.</summary>
/// <param name="customMethodLabel">The label to call the custom method definition.</param>
/// <returns>The custom method created.</returns>
public static AggregationMethodDefinition Custom(string customMethodLabel)
{
ExceptionUtils.CheckArgumentNotNull(customMethodLabel, "customMethodLabel");
// Custom aggregation methods MUST use a namespace-qualified name (see [OData-ABNF]), i.e. contain at least one dot.
Debug.Assert(customMethodLabel.Contains(OData.ExpressionConstants.SymbolDot));
var aggregationMethod = new AggregationMethodDefinition(AggregationMethod.Custom);
aggregationMethod.MethodLabel = customMethodLabel;
return aggregationMethod;
}
}
}
@@ -80,13 +80,13 @@ private AggregateExpression BindAggregateExpressionToken(AggregateExpressionToke
throw new ODataException(ODataErrorStrings.ApplyBinder_AggregateExpressionNotSingleValue(token.Expression));
}
var typeReference = CreateAggregateExpressionTypeReference(expression, token.Method);
var typeReference = CreateAggregateExpressionTypeReference(expression, token.MethodDefinition);
// TODO: Determine source
return new AggregateExpression(expression, token.Method, token.Alias, typeReference);
return new AggregateExpression(expression, token.MethodDefinition, token.Alias, typeReference);
}
private IEdmTypeReference CreateAggregateExpressionTypeReference(SingleValueNode expression, AggregationMethod withVerb)
private IEdmTypeReference CreateAggregateExpressionTypeReference(SingleValueNode expression, AggregationMethodDefinition method)
{
var expressionType = expression.TypeReference;
if (expressionType == null && aggregateExpressionsCache != null)
@@ -98,7 +98,7 @@ private IEdmTypeReference CreateAggregateExpressionTypeReference(SingleValueNode
}
}
switch (withVerb)
switch (method.MethodKind)
{
case AggregationMethod.Average:
var expressionPrimitiveKind = expressionType.PrimitiveKind();
@@ -125,7 +125,10 @@ private IEdmTypeReference CreateAggregateExpressionTypeReference(SingleValueNode
case AggregationMethod.Sum:
return expressionType;
default:
throw new ODataException(ODataErrorStrings.ApplyBinder_UnsupportedAggregateMethod(withVerb));
// Only the EdmModel knows which type the custom aggregation methods returns.
// Since we do not have a reference for it, right now we are assuming that all custom aggregation methods returns Doubles
// TODO: find a appropriate way of getting the return type.
return EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Double, expressionType.IsNullable);
}
}
@@ -285,13 +285,13 @@ internal AggregateExpressionToken ParseAggregateExpression()
// expression
var expression = this.ParseExpression();
var endPathExpression = expression as EndPathToken;
AggregationMethod verb;
AggregationMethodDefinition verb;
// "with" verb
if (endPathExpression != null && endPathExpression.Identifier == ExpressionConstants.QueryOptionCount)
{
// e.g. aggregate($count as Count)
verb = AggregationMethod.VirtualPropertyCount;
verb = AggregationMethodDefinition.VirtualPropertyCount;
}
else
{
@@ -981,7 +981,7 @@ private QueryToken ParseSegment(QueryToken parent)
return new InnerPathToken(propertyName, parent, null);
}
private AggregationMethod ParseAggregateWith()
private AggregationMethodDefinition ParseAggregateWith()
{
if (!TokenIdentifierIs(ExpressionConstants.KeywordWith))
{
@@ -990,30 +990,40 @@ private AggregationMethod ParseAggregateWith()
lexer.NextToken();
AggregationMethod verb;
AggregationMethodDefinition verb;
int identifierStartPosition = lexer.CurrentToken.Position;
string methodLabel = lexer.ReadDottedIdentifier(false /* acceptStar */);
switch (lexer.CurrentToken.GetIdentifier())
switch (methodLabel)
{
case ExpressionConstants.KeywordAverage:
verb = AggregationMethod.Average;
verb = AggregationMethodDefinition.Average;
break;
case ExpressionConstants.KeywordCountDistinct:
verb = AggregationMethod.CountDistinct;
verb = AggregationMethodDefinition.CountDistinct;
break;
case ExpressionConstants.KeywordMax:
verb = AggregationMethod.Max;
verb = AggregationMethodDefinition.Max;
break;
case ExpressionConstants.KeywordMin:
verb = AggregationMethod.Min;
verb = AggregationMethodDefinition.Min;
break;
case ExpressionConstants.KeywordSum:
verb = AggregationMethod.Sum;
verb = AggregationMethodDefinition.Sum;
break;
default:
throw ParseError(ODataErrorStrings.UriQueryExpressionParser_UnrecognizedWithVerb(lexer.CurrentToken.GetIdentifier(), this.lexer.CurrentToken.Position, this.lexer.ExpressionText));
}
if (!methodLabel.Contains(OData.ExpressionConstants.SymbolDot))
{
throw ParseError(
ODataErrorStrings.UriQueryExpressionParser_UnrecognizedWithMethod(
methodLabel,
identifierStartPosition,
this.lexer.ExpressionText));
}
lexer.NextToken();
verb = AggregationMethodDefinition.Custom(methodLabel);
break;
}
return verb;
}
@@ -1071,4 +1081,4 @@ private void RecurseLeave()
this.recursionDepth--;
}
}
}
}
@@ -38,7 +38,7 @@ public void ExpressionSetCorrectly()
}
[Fact]
public void WithVerbSetCorrectly()
public void WithMethodSetCorrectly()
{
var token = new AggregateExpressionToken(expressionToken, AggregationMethod.CountDistinct, "Alias");
token.Method.Should().Be(AggregationMethod.CountDistinct);
Oops, something went wrong.

0 comments on commit d064a1c

Please sign in to comment.