Skip to content

Commit

Permalink
Fix the issue with case insensitive by not set the NextToken for prim…
Browse files Browse the repository at this point in the history
…itive type.

Add more tests
  • Loading branch information
WanjohiSammy committed Nov 17, 2024
1 parent 8483730 commit 26eb9ad
Show file tree
Hide file tree
Showing 3 changed files with 188 additions and 60 deletions.
31 changes: 26 additions & 5 deletions src/Microsoft.OData.Core/UriParser/Binders/FunctionCallBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -234,12 +234,34 @@ internal QueryNode BindFunctionCall(FunctionCallToken functionCallToken)

// If there isn't, bind as Uri function
// Bind all arguments
// Initialize a stack to keep track of previous query tokens
Stack<QueryToken> previousQueryTokens = new Stack<QueryToken>();
List<QueryNode> argumentNodes = functionCallToken.Arguments.Select(argument =>
{
// If the function is IsOf or Cast and the argument is a dotted identifier, we need to bind it differently
if (UnboundFunctionNames.Contains(functionCallToken.Name) && argument.ValueToken is DottedIdentifierToken dottedIdentifier)
if (UnboundFunctionNames.Contains(functionCallToken.Name))
{
return this.TryBindDottedIdentifierForIsOfOrCastFunctionCall(dottedIdentifier);
if(argument.ValueToken is DottedIdentifierToken dottedIdentifier)
{
// Pop the previous query token if available
QueryToken previousArgument = previousQueryTokens.Count > 0 ? previousQueryTokens.Pop() : null;

IEdmSchemaType dottedIdentifierType = UriEdmHelpers.FindTypeFromModel(state.Model, dottedIdentifier.Identifier, this.Resolver);

// If cast or isof has 2 arguments, then the first argument is the next token of the second argument
// If the parent is not a primitive type, set the NextToken of the current dotted identifier to the first argument
if (previousArgument != null && dottedIdentifierType is not IEdmPrimitiveType)
{
// Push the current argument onto the stack to keep track of the previous argument
dottedIdentifier.NextToken = previousArgument;
}

return this.TryBindDottedIdentifierForIsOfOrCastFunctionCall(dottedIdentifier, dottedIdentifierType);
}

// first we need to set the parent of the next argument as the current argument.
// isof and cast can have 1 or 2 arguments, so we need to keep track of the previous argument
previousQueryTokens.Push(argument);
}

return this.bindMethod(argument);
Expand Down Expand Up @@ -440,9 +462,10 @@ private bool TryBindIdentifier(string identifier, IEnumerable<FunctionParameterT
/// Binds a <see cref="DottedIdentifierToken"/> for the 'isof' and 'cast' function calls.
/// </summary>
/// <param name="dottedIdentifierToken">The dotted identifier token to bind.</param>
/// <param name="childType">The child type to bind to.</param>
/// <returns>A <see cref="QueryNode"/> representing the bound single resource node.</returns>
/// <exception cref="ODataException">Thrown when the token cannot be bound as a single resource node.</exception>
private QueryNode TryBindDottedIdentifierForIsOfOrCastFunctionCall(DottedIdentifierToken dottedIdentifierToken)
private QueryNode TryBindDottedIdentifierForIsOfOrCastFunctionCall(DottedIdentifierToken dottedIdentifierToken, IEdmSchemaType childType)
{
QueryNode parent = null;
IEdmType parentType = null;
Expand All @@ -461,8 +484,6 @@ private QueryNode TryBindDottedIdentifierForIsOfOrCastFunctionCall(DottedIdentif
}
}

IEdmSchemaType childType = UriEdmHelpers.FindTypeFromModel(state.Model, dottedIdentifierToken.Identifier, this.Resolver);

if (childType is not IEdmStructuredType childStructuredType)
{
return this.bindMethod(dottedIdentifierToken);
Expand Down
56 changes: 1 addition & 55 deletions src/Microsoft.OData.Core/UriParser/Parsers/FunctionCallParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,6 @@ internal sealed class FunctionCallParser : IFunctionCallParser
/// </summary>
private readonly bool restoreStateIfFail;

/// <summary>
/// If the function call is cast or isof, set to true.
/// </summary>
private bool isFunctionCallNameCastOrIsOf = false;

/// <summary>
/// Create a new FunctionCallParser.
/// </summary>
Expand Down Expand Up @@ -107,9 +102,6 @@ public bool TryParseIdentifierAsFunction(QueryToken parent, out QueryToken resul
this.Lexer.NextToken();
}

// This is used to set the parent of the next argument to the current argument for cast and isof functions.
isFunctionCallNameCastOrIsOf = functionName.SequenceEqual(ExpressionConstants.UnboundFunctionCast.AsSpan()) || functionName.SequenceEqual(ExpressionConstants.UnboundFunctionIsOf.AsSpan());

FunctionParameterToken[] arguments = this.ParseArgumentListOrEntityKeyList(() => lexer.RestorePosition(position));
if (arguments != null)
{
Expand Down Expand Up @@ -188,32 +180,16 @@ public FunctionParameterToken[] ParseArguments()
/// <returns>A list of FunctionParameterTokens representing each argument</returns>
private List<FunctionParameterToken> ReadArgumentsAsPositionalValues()
{
// Store the parent expression of the current argument.
Stack<QueryToken> expressionParents = new Stack<QueryToken>();

List<FunctionParameterToken> argList = new List<FunctionParameterToken>();
while (true)
{
// If we have a parent expression, we need to set the parent of the next argument to the current argument.
QueryToken parentExpression = expressionParents.Count > 0 ? expressionParents.Pop() : null;
QueryToken parameterToken = this.parser.ParseExpression();

// If the function call is cast or isof, set the parent of the next argument to the current argument.
if (parentExpression != null && isFunctionCallNameCastOrIsOf)
{
parameterToken = SetParentForCurrentParameterToken(parentExpression, parameterToken);
}

argList.Add(new FunctionParameterToken(null, parameterToken));
argList.Add(new FunctionParameterToken(null, this.parser.ParseExpression()));
if (this.Lexer.CurrentToken.Kind != ExpressionTokenKind.Comma)
{
break;
}

// In case of comma, we need to parse the next argument
// but first we need to set the parent of the next argument to the current argument.
expressionParents.Push(parameterToken);

this.Lexer.NextToken();
}

Expand All @@ -239,35 +215,5 @@ private bool TryReadArgumentsAsNamedValues(out ICollection<FunctionParameterToke

return false;
}

/// <summary>
/// Set the parent of the next argument to the current argument.
/// For example, in the following query:
/// cast(Location, NS.HomeAddress) where Location is the parentExpression and NS.HomeAddress is the parameterToken.
/// isof(Location, NS.HomeAddress) where Location is the parentExpression and NS.HomeAddress is the parameterToken.
/// </summary>
/// <param name="parentExpression">The previous parameter token.</param>
/// <param name="parameterToken">The current parameter token.</param>
/// <returns>The updated parameter token.</returns>
private static QueryToken SetParentForCurrentParameterToken(QueryToken parentExpression, QueryToken parameterToken)
{
// If the parameter is a dotted identifier, we need to set the parent of the next argument to the current argument.
// But if it is primitive literal, no need to set the parent.
if (parameterToken is DottedIdentifierToken dottedIdentifierToken && dottedIdentifierToken.NextToken == null)
{
// Check if the dottedIdentifier is a primitive type
EdmPrimitiveTypeKind primitiveTypeKind = EdmCoreModel.Instance.GetPrimitiveTypeKind(dottedIdentifierToken.Identifier);

// If the dottedIdentifier is not a primitive type, set the parent of the next argument to the current argument.
// cast(1, Edm.Int32) -> Edm.Int32 is a dottedIdentifierToken but it is a primitive type
if (primitiveTypeKind == EdmPrimitiveTypeKind.None)
{
dottedIdentifierToken.NextToken = parentExpression;
parameterToken = dottedIdentifierToken;
}
}

return parameterToken;
}
}
}
Loading

0 comments on commit 26eb9ad

Please sign in to comment.