Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix 'isof' and 'cast' unquoted type params issue #3117

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1f406c8
For isof and cast, unable to cast type parameters should not throw ex…
WanjohiSammy Nov 12, 2024
981b428
Rename to TryBindDottedIdentifierForIsOfOrCastFunctionCall
WanjohiSammy Nov 12, 2024
a460cbf
Added tests
WanjohiSammy Nov 12, 2024
bab5e32
Remove unnecessary declarations
WanjohiSammy Nov 12, 2024
df8bfe6
Refactor
WanjohiSammy Nov 12, 2024
4d38f5f
Use 'ToList()' instead of 'new List(..)'
WanjohiSammy Nov 13, 2024
8483730
Adding more tests
WanjohiSammy Nov 15, 2024
26eb9ad
Fix the issue with case insensitive by not set the NextToken for prim…
WanjohiSammy Nov 17, 2024
9a76fac
Rename tests correctly
WanjohiSammy Nov 18, 2024
fb4b6b1
Added a few E2E tests
WanjohiSammy Nov 18, 2024
013a49e
Refactor test
WanjohiSammy Nov 18, 2024
e7cd279
Direct check for functioncall cast and isof
WanjohiSammy Nov 18, 2024
bdf86ef
Added more E2E tests
WanjohiSammy Nov 19, 2024
aa5963b
For isof and cast, unable to cast type parameters should not throw ex…
WanjohiSammy Nov 12, 2024
f895c8c
Rename to TryBindDottedIdentifierForIsOfOrCastFunctionCall
WanjohiSammy Nov 12, 2024
6fc5b4b
Added tests
WanjohiSammy Nov 12, 2024
1298f29
Remove unnecessary declarations
WanjohiSammy Nov 12, 2024
b9456c6
Refactor
WanjohiSammy Nov 12, 2024
ac72823
Use 'ToList()' instead of 'new List(..)'
WanjohiSammy Nov 13, 2024
949e08b
Adding more tests
WanjohiSammy Nov 15, 2024
a05ec58
Fix the issue with case insensitive by not set the NextToken for prim…
WanjohiSammy Nov 17, 2024
2513f89
Rename tests correctly
WanjohiSammy Nov 18, 2024
11a4fd0
Added a few E2E tests
WanjohiSammy Nov 18, 2024
8ee5c3f
Refactor test
WanjohiSammy Nov 18, 2024
a46b41f
Direct check for functioncall cast and isof
WanjohiSammy Nov 18, 2024
f2c4d60
Added more E2E tests
WanjohiSammy Nov 19, 2024
44f9896
Add support for unquoted type parameters in isof and cast function call
WanjohiSammy Nov 28, 2024
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
Prev Previous commit
Next Next commit
Fix the issue with case insensitive by not set the NextToken for prim…
…itive type.

Add more tests
WanjohiSammy committed Nov 28, 2024
commit a05ec58d32c065020258afff7ddaccf4cd8f16f3
31 changes: 26 additions & 5 deletions src/Microsoft.OData.Core/UriParser/Binders/FunctionCallBinder.cs
Original file line number Diff line number Diff line change
@@ -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);
@@ -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;
@@ -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);
Original file line number Diff line number Diff line change
@@ -769,6 +769,32 @@ public void IsOfFunctionWorksWithOrWithoutSingleQuotesOnType()
singleValueFunctionCallNode.Parameters.ElementAt(1).ShouldBeConstantQueryNode("Edm.String");
}

[Theory]
[InlineData("isof(ID, Edm.Int64)", "Edm.Int64")]
[InlineData("isof(ID, edm.int64)", "edm.int64")]
[InlineData("isof(ID, Edm.int64)", "Edm.int64")]
[InlineData("isof(ID, EDM.INT64)", "EDM.INT64")]
public void IsOfFunctionWorksWithoutSingleQuotesOnPrimitiveType_CaseInsentive(string queryFilter, string expectedConstantNodeValue)
{
FilterClause filter = ParseFilter(queryFilter, true, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet());
var singleValueFunctionCallNode = filter.Expression.ShouldBeSingleValueFunctionCallQueryNode("isof");
singleValueFunctionCallNode.Parameters.ElementAt(0).ShouldBeSingleValuePropertyAccessQueryNode(HardCodedTestModel.GetPersonIdProp());
singleValueFunctionCallNode.Parameters.ElementAt(1).ShouldBeConstantQueryNode(expectedConstantNodeValue);
}

[Theory]
[InlineData("isof(ID, Edm.Int32)", "Edm.Int32")]
[InlineData("isof(ID, edm.int32)", "edm.int32")]
[InlineData("isof(ID, Edm.int32)", "Edm.int32")]
[InlineData("isof(ID, EDM.INT32)", "EDM.INT32")]
public void IsOfFunctionWorksWithoutSingleQuotesOnPrimitiveType_ODataUriParserCaseInsentive(string queryFilter, string expectedConstantNodeValue)
{
FilterClause filter = ParseFilterODataUriParserCaseInsensitive($"/people?$filter={queryFilter}", HardCodedTestModel.TestModel);
var singleValueFunctionCallNode = filter.Expression.ShouldBeSingleValueFunctionCallQueryNode("isof");
singleValueFunctionCallNode.Parameters.ElementAt(0).ShouldBeSingleValuePropertyAccessQueryNode(HardCodedTestModel.GetPersonIdProp());
singleValueFunctionCallNode.Parameters.ElementAt(1).ShouldBeConstantQueryNode(expectedConstantNodeValue);
}

[Theory]
[InlineData("isof(Fully.Qualified.Namespace.Employee)")]
[InlineData("isof('Fully.Qualified.Namespace.Employee')")]
@@ -830,6 +856,25 @@ public void IsOfFunctionWithOneParameter_WithoutSingleQuotesOnTypeParameter_Shou
Assert.Equal("Fully.Qualified.Namespace.Employee", singleResourceCastNode.TypeReference.FullName()); // Fully.Qualified.Namespace.Employee is the type parameter
}

[Theory]
[InlineData("isof(Fully.Qualified.Namespace.Employee)")]
[InlineData("isof(fully.Qualified.namespace.employee)")]
[InlineData("isof(FULLY.QUALIFIED.NAMESPACE.EMPLOYEE)")]
public void IsOfFunctionWithOneParameter_WithoutSingleQuotesOnTypeParameter_ShouldBeSingleResourceCastNode_CaseInsensitive(string filterQuery)
{
// Arrange
// Act
FilterClause filter = ParseFilterODataUriParserCaseInsensitive($"/people?$filter={filterQuery}", HardCodedTestModel.TestModel);

// Assert
SingleValueFunctionCallNode singleValueFunctionCallNode = filter.Expression.ShouldBeSingleValueFunctionCallQueryNode("isof");
ResourceRangeVariableReferenceNode rangeVariableReference = singleValueFunctionCallNode.Parameters.ElementAt(0).ShouldBeResourceRangeVariableReferenceNode("$it");
Assert.Equal("Fully.Qualified.Namespace.Person", rangeVariableReference.GetEdmTypeReference().FullName()); // $it is of type Person

var singleResourceCastNode = singleValueFunctionCallNode.Parameters.ElementAt(1).ShouldBeSingleResourceCastNode(HardCodedTestModel.GetEmployeeTypeReference());
Assert.Equal("Fully.Qualified.Namespace.Employee", singleResourceCastNode.TypeReference.FullName()); // Fully.Qualified.Namespace.Employee is the type parameter
}

[Theory]
[InlineData("isof(MyAddress, 'Fully.Qualified.Namespace.HomeAddress')")]
[InlineData("isof(MyAddress, Fully.Qualified.Namespace.HomeAddress)")]
@@ -894,6 +939,26 @@ public void IsOfFunctionWithTwoParameters_WithoutSingleQuotesOnTypeParameter_Sho
Assert.Equal("Fully.Qualified.Namespace.HomeAddress", singleResourceCastNode.TypeReference.FullName()); // Fully.Qualified.Namespace.HomeAddress is the type parameter
}

[Theory]
[InlineData("isof(MyAddress, Fully.Qualified.Namespace.HomeAddress)")]
[InlineData("isof(MyAddress, fully.Qualified.namespace.Homeaddress)")]
[InlineData("isof(MyAddress, FULLY.Qualified.Namespace.HOMEAddress)")]
public void IsOfFunctionWithTwoParameters_WithoutSingleQuotesOnTypeParameter_ShouldBeSingleResourceCastNode_CaseInsensitive(string filterQuery)
{
// Arrange
// Act
FilterClause filter = ParseFilterODataUriParserCaseInsensitive($"/people?$filter={filterQuery}", HardCodedTestModel.TestModel);

// Assert
SingleValueFunctionCallNode singleValueFunctionCallNode = filter.Expression.ShouldBeSingleValueFunctionCallQueryNode("isof");
SingleComplexNode singleComplexNode = singleValueFunctionCallNode.Parameters.ElementAt(0).ShouldBeSingleComplexNode(HardCodedTestModel.GetPersonAddressProp());
Assert.Equal("MyAddress", singleComplexNode.Property.Name); // MyAddress is the property name
Assert.Equal("Fully.Qualified.Namespace.Address", singleComplexNode.GetEdmTypeReference().FullName()); // MyAddress is of type Address

var singleResourceCastNode = singleValueFunctionCallNode.Parameters.ElementAt(1).ShouldBeSingleResourceCastNode(HardCodedTestModel.GetHomeAddressReference());
Assert.Equal("Fully.Qualified.Namespace.HomeAddress", singleResourceCastNode.TypeReference.FullName()); // Fully.Qualified.Namespace.HomeAddress is the type parameter
}

[Theory]
[InlineData("isof(Fully.Qualified.Namespace.Pet1)")]
[InlineData("isof(MyAddress,Fully.Qualified.Namespace.Pet1)")]
@@ -934,6 +999,39 @@ public void CastFunctionWorksWithNoSingleQuotesOnType()
bon.Right.ShouldBeConstantQueryNode("blue");
}

[Theory]
[InlineData("cast(Shoe, edm.string) eq 'blue'", "edm.string")]
[InlineData("cast(Shoe, Edm.string) eq 'blue'", "Edm.string")]
[InlineData("cast(Shoe, edm.String) eq 'blue'", "edm.String")]
[InlineData("cast(Shoe, Edm.String) eq 'blue'", "Edm.String")]
[InlineData("cast(Shoe, EDM.STRING) eq 'blue'", "EDM.STRING")]
public void CastFunctionWorksWithNoSingleQuotesOnTypeWithODataUriParserCaseInsensitive(string filterQuery, string expectedConstantQueryNode)
{
FilterClause filter = ParseFilterODataUriParserCaseInsensitive($"/people?$filter={filterQuery}", HardCodedTestModel.TestModel);
var bon = filter.Expression.ShouldBeBinaryOperatorNode(BinaryOperatorKind.Equal);
var convertQueryNode = bon.Left.ShouldBeConvertQueryNode(EdmPrimitiveTypeKind.String);
var singleFunctionCallNode = convertQueryNode.Source.ShouldBeSingleValueFunctionCallQueryNode("cast");
singleFunctionCallNode.Parameters.ElementAt(0).ShouldBeSingleValuePropertyAccessQueryNode(HardCodedTestModel.GetPersonShoeProp());
singleFunctionCallNode.Parameters.ElementAt(1).ShouldBeConstantQueryNode(expectedConstantQueryNode);
bon.Right.ShouldBeConstantQueryNode("blue");
}

[Theory]
[InlineData("cast(ID, edm.int32) lt 10", "edm.int32")]
[InlineData("cast(ID, Edm.int32) lt 10", "Edm.int32")]
[InlineData("cast(ID, edm.Int32) lt 10", "edm.Int32")]
[InlineData("cast(ID, Edm.Int32) lt 10", "Edm.Int32")]
[InlineData("cast(ID, EDM.INT32) lt 10", "EDM.INT32")]
public void CastFunctionWorksWithNoSingleQuotesOnTypeWithCaseInsensitive(string filterQuery, string expectedConstantQueryNode)
{
FilterClause filter = ParseFilter(filterQuery, true, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet());
var bon = filter.Expression.ShouldBeBinaryOperatorNode(BinaryOperatorKind.LessThan);
var singleFunctionCallNode = bon.Left.ShouldBeSingleValueFunctionCallQueryNode("cast");
singleFunctionCallNode.Parameters.ElementAt(0).ShouldBeSingleValuePropertyAccessQueryNode(HardCodedTestModel.GetPersonIdProp());
singleFunctionCallNode.Parameters.ElementAt(1).ShouldBeConstantQueryNode(expectedConstantQueryNode);
bon.Right.ShouldBeConstantQueryNode(10);
}

[Fact]
public void CastFunctionWorksForEnum()
{
@@ -1009,6 +1107,45 @@ public void CastFunctionProducesAnEntityType(string filterQuery)
Assert.IsType<BinaryOperatorNode>(filter.Expression).Right.ShouldBeConstantQueryNode("blue");
}

[Theory]
[InlineData("cast(MyDog, fully.Qualified.Namespace.Dog)/Color eq 'blue'")]
[InlineData("cast(MyDog, fully.qualified.namespace.dog)/Color eq 'blue'")]
[InlineData("cast(MyDog, fully.Qualified.namespace.dog)/Color eq 'blue'")]
[InlineData("cast(MyDog, fully.qualified.Namespace.dog)/Color eq 'blue'")]
[InlineData("cast(MyDog, fully.qualified.namespace.Dog)/Color eq 'blue'")]
[InlineData("cast(MyDog, FULLY.QUALIFIED.NAMESPACE.DOG)/Color eq 'blue'")]
public void CastFunctionProducesAnEntityTypeWorksWithCaseInsensitiveODataUriParser(string filterQuery)
{
FilterClause filter = ParseFilter(filterQuery, true, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet());
SingleResourceFunctionCallNode function = filter.Expression.ShouldBeBinaryOperatorNode(BinaryOperatorKind.Equal)
.Left.ShouldBeSingleValuePropertyAccessQueryNode(HardCodedTestModel.GetDogColorProp())
.Source.ShouldBeSingleResourceFunctionCallNode("cast");
Assert.Equal(2, function.Parameters.Count());
function.Parameters.ElementAt(0).ShouldBeSingleNavigationNode(HardCodedTestModel.GetPersonMyDogNavProp());
var singleResourceCastNode = function.Parameters.ElementAt(1).ShouldBeSingleCastNode(HardCodedTestModel.GetDogTypeReference());
Assert.Equal("Fully.Qualified.Namespace.Dog", singleResourceCastNode.TypeReference.FullName());
Assert.IsType<BinaryOperatorNode>(filter.Expression).Right.ShouldBeConstantQueryNode("blue");
}

[Theory]
[InlineData("cast(MyDog, fully.Qualified.Namespace.Dog)/Color eq 'blue'")]
[InlineData("cast(MyDog, fully.qualified.namespace.dog)/Color eq 'blue'")]
[InlineData("cast(MyDog, fully.Qualified.namespace.dog)/Color eq 'blue'")]
[InlineData("cast(MyDog, fully.qualified.Namespace.dog)/Color eq 'blue'")]
[InlineData("cast(MyDog, fully.qualified.namespace.Dog)/Color eq 'blue'")]
public void CastFunctionProducesAnEntityTypeWorksWithCaseInsensitive(string filterQuery)
{
FilterClause filter = ParseFilterODataUriParserCaseInsensitive($"/people?$filter={filterQuery}", HardCodedTestModel.TestModel);
SingleResourceFunctionCallNode function = filter.Expression.ShouldBeBinaryOperatorNode(BinaryOperatorKind.Equal)
.Left.ShouldBeSingleValuePropertyAccessQueryNode(HardCodedTestModel.GetDogColorProp())
.Source.ShouldBeSingleResourceFunctionCallNode("cast");
Assert.Equal(2, function.Parameters.Count());
function.Parameters.ElementAt(0).ShouldBeSingleNavigationNode(HardCodedTestModel.GetPersonMyDogNavProp());
var singleResourceCastNode = function.Parameters.ElementAt(1).ShouldBeSingleCastNode(HardCodedTestModel.GetDogTypeReference());
Assert.Equal("Fully.Qualified.Namespace.Dog", singleResourceCastNode.TypeReference.FullName());
Assert.IsType<BinaryOperatorNode>(filter.Expression).Right.ShouldBeConstantQueryNode("blue");
}

[Fact]
public void OrderByWithExpression()
{
@@ -3202,6 +3339,30 @@ private static FilterClause ParseFilter(string text, IEdmModel edmModel, IEdmTyp
return new ODataQueryOptionParser(edmModel, edmType, edmEntitySet, new Dictionary<string, string>() { { "$filter", text } }) { Resolver = new ODataUriResolver() { EnableCaseInsensitive = false } }.ParseFilter();
}

private static FilterClause ParseFilter(string text, bool caseInsensitive, IEdmModel edmModel, IEdmType edmType, IEdmNavigationSource edmEntitySet = null)
{
return new ODataQueryOptionParser(edmModel,
edmType,
edmEntitySet,
new Dictionary<string, string>() { { "$filter", text } }) { Resolver = new ODataUriResolver() { EnableCaseInsensitive = caseInsensitive } }
.ParseFilter();
}

private static FilterClause ParseFilterODataUriParserCaseInsensitive(string text, IEdmModel edmModel)
{
var parser = new ODataUriParser(edmModel, new Uri(text, UriKind.Relative))
{
Resolver = new UnqualifiedODataUriResolver()
{
EnableCaseInsensitive = true,
},
UrlKeyDelimiter = ODataUrlKeyDelimiter.Slash,
};
parser.Settings.MaximumExpansionDepth = 2;
parser.ParsePath();
return parser.ParseFilter();
}

private static OrderByClause ParseOrderBy(string text, IEdmModel edmModel, IEdmType edmType, IEdmNavigationSource edmEntitySet = null)
{
return new ODataQueryOptionParser(edmModel, edmType, edmEntitySet, new Dictionary<string, string>() { { "$orderby", text } }).ParseOrderBy();