Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
8acf405
Refactor
WanjohiSammy Apr 1, 2025
065d26c
Remove unnecessary changes
WanjohiSammy Apr 1, 2025
425b42c
Remove unnecessary whitespaces
WanjohiSammy Apr 1, 2025
082a1ed
Remove whitespaces
WanjohiSammy Apr 1, 2025
6162161
Remove unnecessary whitespaces
WanjohiSammy Apr 1, 2025
b267f82
Remove the private method and just pass the functionName as parameter
WanjohiSammy Jul 9, 2025
421cbbf
Merge branch 'main' into fix/impossible-cast-with-isof-exception
WanjohiSammy Jul 9, 2025
0d37d6c
nit
WanjohiSammy Jul 9, 2025
41a440f
Merge branch 'fix/impossible-cast-with-isof-exception' of https://git…
WanjohiSammy Jul 9, 2025
3e33ff6
Refactor
WanjohiSammy Apr 1, 2025
f431035
Remove unnecessary changes
WanjohiSammy Apr 1, 2025
c69fd1c
Remove unnecessary whitespaces
WanjohiSammy Apr 1, 2025
7cf4fa7
Remove whitespaces
WanjohiSammy Apr 1, 2025
53e2015
Remove unnecessary whitespaces
WanjohiSammy Apr 1, 2025
6bdf585
Remove the private method and just pass the functionName as parameter
WanjohiSammy Jul 9, 2025
049fae6
nit
WanjohiSammy Jul 9, 2025
0679cdf
Update src/Microsoft.OData.Core/UriParser/Parsers/FunctionCallParser.cs
WanjohiSammy Aug 6, 2025
5f1c503
refactor and merge with origin main
WanjohiSammy Aug 6, 2025
51b1da4
Merge remote-tracking branch 'origin' into fix/impossible-cast-with-i…
WanjohiSammy Aug 6, 2025
7b2a775
Use of NormalizedModelElementsCache
WanjohiSammy Aug 6, 2025
c5ba104
using index
WanjohiSammy Aug 6, 2025
7ebd963
nit
WanjohiSammy Aug 6, 2025
d7e5211
using SchemaElements
WanjohiSammy Aug 6, 2025
8644a6e
rename tests correctly
WanjohiSammy Aug 7, 2025
04f2f61
Add NormalizedModelElementsCache singleton for EdmCoreModel and use i…
WanjohiSammy Aug 7, 2025
1b7e1c6
FindSchemaTypes can return null
WanjohiSammy Aug 7, 2025
f219ef3
Use a lightweight stack struct
WanjohiSammy Aug 7, 2025
106199f
Make StackStruct internal to avoid exposing it to the Public API
WanjohiSammy Aug 7, 2025
76e644d
move StackStructOfT to Microsoft.OData namespace
WanjohiSammy Aug 7, 2025
a6e59a8
use explicit types
WanjohiSammy Aug 8, 2025
a8d7bce
Update src/Microsoft.OData.Core/UriParser/Resolver/NormalizedModelEle…
WanjohiSammy Aug 8, 2025
785ac02
Use regular Stack
WanjohiSammy Aug 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions src/Microsoft.OData.Core/UriParser/Binders/FunctionCallBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -720,12 +720,19 @@ private static IEdmTypeReference ValidateIsOfOrCast(BindingState state, bool isC
Error.Format(SRResources.MetadataBinder_CastOrIsOfExpressionWithWrongNumberOfOperands, args.Count));
}

ConstantNode typeArgument = args.Last() as ConstantNode;
QueryNode queryNode = args[^1];

string typeArgumentFullName = null;
IEdmTypeReference returnType = null;
if (typeArgument != null)
if (queryNode is SingleResourceCastNode singleResourceCastNode)
{
returnType = TryGetTypeReference(state.Model, typeArgument.Value as string, state.Configuration.Resolver);
returnType = singleResourceCastNode.TypeReference;
typeArgumentFullName = returnType.FullName();
}
else if (queryNode is ConstantNode constantNode)
{
typeArgumentFullName = constantNode.Value as string;
returnType = TryGetTypeReference(state.Model, typeArgumentFullName, state.Configuration.Resolver);
}

if (returnType == null)
Expand Down Expand Up @@ -758,7 +765,7 @@ private static IEdmTypeReference ValidateIsOfOrCast(BindingState state, bool isC
{
// throw if cast enum to not-string :
if ((args[0].GetEdmTypeReference() is IEdmEnumTypeReference)
&& !string.Equals(typeArgument.Value as string, Microsoft.OData.Metadata.EdmConstants.EdmStringTypeName, StringComparison.Ordinal))
&& !string.Equals(typeArgumentFullName, Microsoft.OData.Metadata.EdmConstants.EdmStringTypeName, state.Configuration.Resolver?.EnableCaseInsensitive == true ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))
{
throw new ODataException(SRResources.CastBinder_EnumOnlyCastToOrFromString);
}
Expand Down
66 changes: 59 additions & 7 deletions src/Microsoft.OData.Core/UriParser/Parsers/FunctionCallParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ namespace Microsoft.OData.UriParser
using System.Diagnostics;
using System.Linq;
using Microsoft.OData.Core;
using Microsoft.OData.Edm;

/// <summary>
/// Implementation of IFunctionCallParser that allows functions calls and parses arguments with a provided method.
Expand Down Expand Up @@ -101,7 +102,7 @@ public bool TryParseIdentifierAsFunction(QueryToken parent, out QueryToken resul
this.Lexer.NextToken();
}

FunctionParameterToken[] arguments = this.ParseArgumentListOrEntityKeyList(() => lexer.RestorePosition(position));
FunctionParameterToken[] arguments = this.ParseArgumentListOrEntityKeyList(() => lexer.RestorePosition(position), functionName);
if (arguments != null)
{
result = new FunctionCallToken(functionName.ToString(), arguments, parent);
Expand All @@ -114,8 +115,9 @@ public bool TryParseIdentifierAsFunction(QueryToken parent, out QueryToken resul
/// Parses argument lists or entity key value list.
/// </summary>
/// <param name="restoreAction">Action invoked for restoring a state during failure.</param>
/// <param name="functionName">The name of the function being called. Default is an empty span.</param>
/// <returns>The lexical tokens representing the arguments.</returns>
public FunctionParameterToken[] ParseArgumentListOrEntityKeyList(Action restoreAction = null)
public FunctionParameterToken[] ParseArgumentListOrEntityKeyList(Action restoreAction = null, ReadOnlySpan<char> functionName = default)
{
if (this.Lexer.CurrentToken.Kind != ExpressionTokenKind.OpenParen)
{
Expand All @@ -136,7 +138,7 @@ public FunctionParameterToken[] ParseArgumentListOrEntityKeyList(Action restoreA
}
else
{
arguments = this.ParseArguments();
arguments = this.ParseArguments(functionName);
}

if (this.Lexer.CurrentToken.Kind != ExpressionTokenKind.CloseParen)
Expand All @@ -161,33 +163,53 @@ public FunctionParameterToken[] ParseArgumentListOrEntityKeyList(Action restoreA
/// Arguments can either be of the form a=1,b=2,c=3 or 1,2,3.
/// They cannot be mixed between those two styles.
/// </remarks>
/// <param name="functionName">The name of the function being called. Default is an empty span.</param>
/// <returns>The lexical tokens representing the arguments.</returns>
public FunctionParameterToken[] ParseArguments()
public FunctionParameterToken[] ParseArguments(ReadOnlySpan<char> functionName = default)
{
ICollection<FunctionParameterToken> argList;
if (this.TryReadArgumentsAsNamedValues(out argList))
{
return argList.ToArray();
}

return this.ReadArgumentsAsPositionalValues().ToArray();
return this.ReadArgumentsAsPositionalValues(functionName).ToArray();
}

/// <summary>
/// Read the list of arguments as a set of positional values
/// </summary>
/// <param name="functionName">The name of the function being called. Default is an empty span.</param>
/// <returns>A list of FunctionParameterTokens representing each argument</returns>
private List<FunctionParameterToken> ReadArgumentsAsPositionalValues()
private List<FunctionParameterToken> ReadArgumentsAsPositionalValues(ReadOnlySpan<char> functionName = default)
{
// Store the parent expression of the current argument.
Stack<QueryToken> expressionParents = new Stack<QueryToken>();
bool isFunctionCallNameCastOrIsOf = functionName.Length > 0 &&
(functionName.SequenceEqual(ExpressionConstants.UnboundFunctionCast.AsSpan()) || functionName.SequenceEqual(ExpressionConstants.UnboundFunctionIsOf.AsSpan()));
List<FunctionParameterToken> argList = new List<FunctionParameterToken>();
while (true)
{
argList.Add(new FunctionParameterToken(null, this.parser.ParseExpression()));
// 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));
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 @@ -213,5 +235,35 @@ 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 (parameterToken is not DottedIdentifierToken dottedIdentifierToken || dottedIdentifierToken?.NextToken is not null)
{
return parameterToken;
}

// Check if the identifier is a primitive type
IEdmSchemaType schemaType = NormalizedModelElementsCache.EdmCoreModelInstance.FindSchemaTypes(dottedIdentifierToken.Identifier)?.FirstOrDefault();

// If the identifier is a primitive type
if (schemaType is IEdmPrimitiveType)
{
return parameterToken;
}

// Set the parent of the next argument to the current argument
dottedIdentifierToken.NextToken = parentExpression;
return dottedIdentifierToken;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ namespace Microsoft.OData.Edm
/// </summary>
internal sealed class NormalizedModelElementsCache
{
/// <summary>
/// Provides a shared, normalized case-insensitive cache of model elements for the EDM core model.
/// </summary>
public static readonly NormalizedModelElementsCache EdmCoreModelInstance = new NormalizedModelElementsCache(EdmCoreModel.Instance);

// We create different caches for different types of schema elements because all current usage request schema elements
// of specific types. If we were to use a single dictionary <string, ISchemaElement> we would need
// to do additional work (and allocations) during lookups to filter the results to the subset that matches the request type.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -410,20 +410,26 @@ public void ParseFilterWithNullEnumValue()
convertNode.Source.ShouldBeConstantQueryNode((object)null);
}

[Fact]
public void ParseFilterCastMethod1()
[Theory]
[InlineData("cast(NS.Color'Green', 'Edm.String') eq 'blue'")]
[InlineData("cast(NS.Color'Green', Edm.String) eq 'blue'")]
[InlineData("cast(NS.Color'Green', edm.string) eq 'blue'")]
[InlineData("cast(NS.Color'Green', EDM.STRING) eq 'blue'")]
public void ParseFilterCastMethodWithEdmPrimitiveTypes(string filterQuery)
{
var filter = ParseFilter("cast(NS.Color'Green', 'Edm.String') eq 'blue'", this.userModel, this.entityType, this.entitySet);
var filter = ParseFilter(filterQuery, true, this.userModel, this.entityType, this.entitySet);
var bon = filter.Expression.ShouldBeBinaryOperatorNode(BinaryOperatorKind.Equal);
var convertNode = Assert.IsType<ConvertNode>(bon.Left);
var functionCallNode = Assert.IsType<SingleValueFunctionCallNode>(convertNode.Source);
Assert.Equal("cast", functionCallNode.Name); // ConvertNode is because cast() result's nullable=false.
}

[Fact]
public void ParseFilterCastMethod2()
[Theory]
[InlineData("cast('Green', 'NS.Color') eq NS.Color'Green'")]
[InlineData("cast('Green', NS.Color) eq NS.Color'Green'")]
public void ParseFilterCastMethodWithOrWithoutSingleQuotesOnType(string filterQuery)
{
var filter = ParseFilter("cast('Green', 'NS.Color') eq NS.Color'Green'", this.userModel, this.entityType, this.entitySet);
var filter = ParseFilter(filterQuery, this.userModel, this.entityType, this.entitySet);
var bon = filter.Expression.ShouldBeBinaryOperatorNode(BinaryOperatorKind.Equal);
var functionCallNode = Assert.IsType<SingleValueFunctionCallNode>(bon.Left);
Assert.Equal("cast", functionCallNode.Name);
Expand Down Expand Up @@ -500,17 +506,20 @@ public void ParseFilterEnumMemberUndefined4()
parse.Throws<ODataException>(Error.Format(SRResources.Binder_IsNotValidEnumConstant, "NS.ColorFlags'Red,2'"));
}

[Fact]
public void ParseFilterEnumTypesWrongCast1()
[Theory]
[InlineData("cast(NS.ColorFlags'Green', 'Edm.Int64') eq 2")]
[InlineData("cast(NS.ColorFlags'Green', Edm.Int64) eq 2")]
public void ParseFilterEnumTypesWrongCast1(string filter)
{
Action parse = () => ParseFilter("cast(NS.ColorFlags'Green', 'Edm.Int64') eq 2", this.userModel, this.entityType, this.entitySet);
Action parse = () => ParseFilter(filter, this.userModel, this.entityType, this.entitySet);
parse.Throws<ODataException>(SRResources.CastBinder_EnumOnlyCastToOrFromString);
}

[Fact]
public void ParseFilterEnumTypesWrongCast2()
[Theory]
[InlineData("cast(321, 'NS.ColorFlags') eq 2")]
[InlineData("cast(321, NS.ColorFlags) eq 2")]
public void ParseFilterEnumTypesWrongCast2(string filter)
{
Action parse = () => ParseFilter("cast(321, 'NS.ColorFlags') eq 2", this.userModel, this.entityType, this.entitySet);
Action parse = () => ParseFilter(filter, this.userModel, this.entityType, this.entitySet);
parse.Throws<ODataException>(SRResources.CastBinder_EnumOnlyCastToOrFromString);
}

Expand All @@ -536,7 +545,7 @@ public void ParseFilterWithInOperatorWithEnums(string filterOptionValue)
IEdmEnumType colorType = this.GetIEdmType<IEdmEnumType>(colorTypeName);
IEdmEnumMember enumMember = colorType.Members.First(m => m.Name == enumValue);

string expectedLiteral = "'Green'";
string expectedLiteral = "'Green'";
if (filterOptionValue.StartsWith(colorTypeName)) // if the filterOptionValue is already fully qualified, then the expectedLiteral should be the same as filterOptionValue
{
expectedLiteral = "NS.Color'Green'";
Expand Down Expand Up @@ -651,5 +660,11 @@ private FilterClause ParseFilter(string text, IEdmModel edmModel, IEdmEntityType
{
return new ODataQueryOptionParser(edmModel, entityType, edmEntitySet, new Dictionary<string, string>() { { "$filter", text } }).ParseFilter();
}

private FilterClause ParseFilter(string text, bool caseInsensitive, IEdmModel edmModel, IEdmEntityType edmEntityType, IEdmEntitySet edmEntitySet)
{
return new ODataQueryOptionParser(edmModel, entityType, edmEntitySet, new Dictionary<string, string>() { { "$filter", text } })
Comment on lines 661 to +666
Copy link

Copilot AI Aug 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method parameter 'edmEntityType' is used but the local variable 'entityType' is passed to the constructor. This should be 'edmEntityType' to match the parameter name and avoid potential confusion.

Suggested change
return new ODataQueryOptionParser(edmModel, entityType, edmEntitySet, new Dictionary<string, string>() { { "$filter", text } }).ParseFilter();
}
private FilterClause ParseFilter(string text, bool caseInsensitive, IEdmModel edmModel, IEdmEntityType edmEntityType, IEdmEntitySet edmEntitySet)
{
return new ODataQueryOptionParser(edmModel, entityType, edmEntitySet, new Dictionary<string, string>() { { "$filter", text } })
return new ODataQueryOptionParser(edmModel, edmEntityType, edmEntitySet, new Dictionary<string, string>() { { "$filter", text } }).ParseFilter();
}
private FilterClause ParseFilter(string text, bool caseInsensitive, IEdmModel edmModel, IEdmEntityType edmEntityType, IEdmEntitySet edmEntitySet)
{
return new ODataQueryOptionParser(edmModel, edmEntityType, edmEntitySet, new Dictionary<string, string>() { { "$filter", text } })

Copilot uses AI. Check for mistakes.
{ Resolver = new ODataUriResolver() { EnableCaseInsensitive = caseInsensitive } }.ParseFilter();
}
}
}
Loading