diff --git a/src/Microsoft.OData.Core/UriParser/Binders/FunctionCallBinder.cs b/src/Microsoft.OData.Core/UriParser/Binders/FunctionCallBinder.cs index d8c70e78cd..4985c1d67e 100644 --- a/src/Microsoft.OData.Core/UriParser/Binders/FunctionCallBinder.cs +++ b/src/Microsoft.OData.Core/UriParser/Binders/FunctionCallBinder.cs @@ -233,8 +233,38 @@ internal QueryNode BindFunctionCall(FunctionCallToken functionCallToken) } // If there isn't, bind as Uri function + // Initialize a stack to keep track of previous query tokens + Stack previousQueryTokens = new Stack(); + // Bind all arguments - List argumentNodes = new List(functionCallToken.Arguments.Select(ar => this.bindMethod(ar))); + List 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 + string functionCallTokenName = IsUnboundFunction(functionCallToken.Name); + if ((ExpressionConstants.UnboundFunctionIsOf == functionCallTokenName || ExpressionConstants.UnboundFunctionCast == functionCallTokenName) && argument.ValueToken is DottedIdentifierToken dottedIdentifier) + { + // Pop the previous query token if available + QueryToken previousArgument = previousQueryTokens.Count > 0 ? previousQueryTokens.Pop() : null; + + // Find the type of the dotted identifier by resolving it against the model. This also ensure case-insensitive resolution. + IEdmSchemaType dottedIdentifierType = UriEdmHelpers.FindTypeFromModel(state.Model, dottedIdentifier.Identifier, this.Resolver); + + // If the dotted identifier is not a primitive type, set the next token to the previous argument + if (dottedIdentifierType is not IEdmPrimitiveType && previousArgument != null) + { + // Set the next token of the dotted identifier to the previous argument + dottedIdentifier.NextToken = previousArgument; + } + + // isof and cast can have 1 or 2 arguments, so we need to keep track of the previous argument + previousQueryTokens.Push(argument); + + return this.TryBindDottedIdentifierForIsOfOrCastFunctionCall(dottedIdentifier, dottedIdentifierType); + } + + return this.bindMethod(argument); + }).ToList(); + return BindAsUriFunction(functionCallToken, argumentNodes); } @@ -426,6 +456,50 @@ private bool TryBindIdentifier(string identifier, IEnumerable + /// Binds a for the 'isof' and 'cast' function calls. + /// + /// The dotted identifier token to bind. + /// The child type to bind to. + /// A representing the bound single resource node. + /// Thrown when the token cannot be bound as a single resource node. + private QueryNode TryBindDottedIdentifierForIsOfOrCastFunctionCall(DottedIdentifierToken dottedIdentifierToken, IEdmSchemaType childType) + { + QueryNode parent = null; + IEdmType parentType = null; + + if (state.ImplicitRangeVariable != null) + { + if (dottedIdentifierToken.NextToken == null) + { + parent = NodeFactory.CreateRangeVariableReferenceNode(state.ImplicitRangeVariable); + parentType = state.ImplicitRangeVariable.TypeReference.Definition; + } + else + { + parent = this.bindMethod(dottedIdentifierToken.NextToken); + parentType = parent.GetEdmType(); + } + } + + if (childType is not IEdmStructuredType childStructuredType) + { + return this.bindMethod(dottedIdentifierToken); + } + + if (parentType != null) + { + this.state.ParsedSegments.Add(new TypeSegment(childType, parentType, null)); + } + + if (parent is CollectionResourceNode parentAsCollection) + { + return new CollectionResourceCastNode(parentAsCollection, childStructuredType); + } + + return new SingleResourceCastNode(parent as SingleResourceNode, childStructuredType); + } + /// /// Bind path segment's operation or operationImport's parameters. /// @@ -720,12 +794,16 @@ private static IEdmTypeReference ValidateIsOfOrCast(BindingState state, bool isC ODataErrorStrings.MetadataBinder_CastOrIsOfExpressionWithWrongNumberOfOperands(args.Count)); } - ConstantNode typeArgument = args.Last() as ConstantNode; + QueryNode queryNode = args.Last(); IEdmTypeReference returnType = null; - if (typeArgument != null) + if (queryNode is SingleResourceCastNode singleResourceCastNode) + { + returnType = singleResourceCastNode.TypeReference; + } + else if (queryNode is ConstantNode constantNode) { - returnType = TryGetTypeReference(state.Model, typeArgument.Value as string, state.Configuration.Resolver); + returnType = TryGetTypeReference(state.Model, constantNode.Value as string, state.Configuration.Resolver); } if (returnType == null) @@ -758,7 +836,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(returnType.FullName(), Microsoft.OData.Metadata.EdmConstants.EdmStringTypeName, StringComparison.Ordinal)) { throw new ODataException(ODataErrorStrings.CastBinder_EnumOnlyCastToOrFromString); } diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/EnumFilterFunctionalTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/EnumFilterFunctionalTests.cs index 64b9bb37b9..439e391b1b 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/EnumFilterFunctionalTests.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/EnumFilterFunctionalTests.cs @@ -409,20 +409,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(bon.Left); var functionCallNode = Assert.IsType(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(bon.Left); Assert.Equal("cast", functionCallNode.Name); @@ -499,17 +505,21 @@ public void ParseFilterEnumMemberUndefined4() parse.Throws(Strings.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(Strings.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(Strings.CastBinder_EnumOnlyCastToOrFromString); } @@ -650,5 +660,11 @@ private FilterClause ParseFilter(string text, IEdmModel edmModel, IEdmEntityType { return new ODataQueryOptionParser(edmModel, entityType, edmEntitySet, new Dictionary() { { "$filter", text } }).ParseFilter(); } + + private FilterClause ParseFilter(string text, bool caseInsensitive, IEdmModel edmModel, IEdmEntityType edmEntityType, IEdmEntitySet edmEntitySet) + { + return new ODataQueryOptionParser(edmModel, entityType, edmEntitySet, new Dictionary() { { "$filter", text } }) + { Resolver = new ODataUriResolver() { EnableCaseInsensitive = caseInsensitive } }.ParseFilter(); + } } } 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 4cdc0a6e11..bfd9b1f275 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/FilterAndOrderByFunctionalTests.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/FilterAndOrderByFunctionalTests.cs @@ -760,6 +760,233 @@ public void IsOfFunctionWorksWithSingleQuotesOnType() singleValueFunctionCallNode.Parameters.ElementAt(1).ShouldBeConstantQueryNode("Edm.String"); } + [Fact] + public void IsOfFunctionWorksWithOrWithoutSingleQuotesOnType() + { + FilterClause filter = ParseFilter("isof(Shoe, Edm.String)", HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet()); + var singleValueFunctionCallNode = filter.Expression.ShouldBeSingleValueFunctionCallQueryNode("isof"); + singleValueFunctionCallNode.Parameters.ElementAt(0).ShouldBeSingleValuePropertyAccessQueryNode(HardCodedTestModel.GetPersonShoeProp()); + 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')")] + public void IsOfFunctionWithOneParameter_WithOrWithoutSingleQuotesOnTypeParameter_WorksAsExpected(string filterQuery) + { + // Arrange & Act + FilterClause filter = ParseFilter(filterQuery, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet()); + + // 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 + + if(singleValueFunctionCallNode.Parameters.ElementAt(1) is ConstantNode) + { + var constantNode = singleValueFunctionCallNode.Parameters.ElementAt(1).ShouldBeConstantQueryNode("Fully.Qualified.Namespace.Employee"); + Assert.Equal("Fully.Qualified.Namespace.Employee", constantNode.Value); + } + else + { + var singleResourceCastNode = singleValueFunctionCallNode.Parameters.ElementAt(1).ShouldBeSingleResourceCastNode(HardCodedTestModel.GetEmployeeTypeReference()); + Assert.Equal("Fully.Qualified.Namespace.Employee", singleResourceCastNode.TypeReference.FullName()); + } + } + + [Fact] + public void IsOfFunctionWithOneParameter_WithSingleQuotesOnTypeParameter_ShouldBeConstantQueryNode() + { + // Arrange + var filterQuery = "isof('Fully.Qualified.Namespace.Employee')"; + + // Act + FilterClause filter = ParseFilter(filterQuery, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet()); + + // 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 constantNode = singleValueFunctionCallNode.Parameters.ElementAt(1).ShouldBeConstantQueryNode("Fully.Qualified.Namespace.Employee"); + Assert.Equal("Fully.Qualified.Namespace.Employee", constantNode.Value); // 'Fully.Qualified.Namespace.Employee' is the type parameter + } + + [Fact] + public void IsOfFunctionWithOneParameter_WithoutSingleQuotesOnTypeParameter_ShouldBeSingleResourceCastNode() + { + // Arrange + var filterQuery = "isof(Fully.Qualified.Namespace.Employee)"; + + // Act + FilterClause filter = ParseFilter(filterQuery, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet()); + + // 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(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)")] + public void IsOfFunctionWithTwoParameters_WithSingleQuotesOnTypeParameter_WorksAsExpected(string filterQuery) + { + // Arrange & Act + FilterClause filter = ParseFilter(filterQuery, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet()); + + // 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 + + if (singleValueFunctionCallNode.Parameters.ElementAt(1) is ConstantNode) + { + var constantNode = singleValueFunctionCallNode.Parameters.ElementAt(1).ShouldBeConstantQueryNode("Fully.Qualified.Namespace.HomeAddress"); + Assert.Equal("Fully.Qualified.Namespace.HomeAddress", constantNode.Value); // 'Fully.Qualified.Namespace.Employee' is the type parameter + } + else + { + 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 + } + } + + [Fact] + public void IsOfFunctionWithTwoParameters_WithSingleQuotesOnTypeParameter_ShouldBeConstantQueryNode() + { + // Arrange + var filterQuery = "isof(MyAddress, 'Fully.Qualified.Namespace.HomeAddress')"; + + // Act + FilterClause filter = ParseFilter(filterQuery, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet()); + + // 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 constantNode = singleValueFunctionCallNode.Parameters.ElementAt(1).ShouldBeConstantQueryNode("Fully.Qualified.Namespace.HomeAddress"); + Assert.Equal("Fully.Qualified.Namespace.HomeAddress", constantNode.Value); // 'Fully.Qualified.Namespace.Employee' is the type parameter + } + + [Fact] + public void IsOfFunctionWithTwoParameters_WithoutSingleQuotesOnTypeParameter_ShouldBeSingleResourceCastNode() + { + // Arrange + var filterQuery = "isof(MyAddress, Fully.Qualified.Namespace.HomeAddress)"; + + // Act + FilterClause filter = ParseFilter(filterQuery, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet()); + + // 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(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)")] + [InlineData("isof(null,Fully.Qualified.Namespace.Person)")] + [InlineData("isof('',Fully.Qualified.Namespace.Person)")] + public void IsOfFunctionsWithUnquotedTypeParameter_WithIncorrectType_DoesNotThrowException(string filterQuery) + { + // Arrange & Act + var exception = Record.Exception(() => ParseFilter(filterQuery, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet())); + + // Assert + Assert.Null(exception); + } + + [Theory] + [InlineData("cast(Fully.Qualified.Namespace.HomeAddress)/City eq 'City1'")] + [InlineData("cast(MyAddress,Fully.Qualified.Namespace.Employee)/WorkID eq 345")] + [InlineData("cast(null,Fully.Qualified.Namespace.Employee)/WorkID eq 345")] + [InlineData("cast('',Fully.Qualified.Namespace.Employee)/WorkID eq 345")] + public void CastFunctionWithUnquotedTypeParameter_WithIncorrectType_DoesNotThrowException(string filterQuery) + { + // Arrange & Act + var exception = Record.Exception(() => ParseFilter(filterQuery, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet())); + + // Assert + Assert.Null(exception); + } + [Fact] public void CastFunctionWorksWithNoSingleQuotesOnType() { @@ -772,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() { @@ -783,14 +1043,31 @@ public void CastFunctionWorksForEnum() bon.Right.ShouldBeEnumNode(HardCodedTestModel.TestModel.FindType("Fully.Qualified.Namespace.ColorPattern") as IEdmEnumType, 2L); } - [Fact] - public void CastFunctionWorksForCastFromNullToEnum() + [Theory] + [InlineData("cast(null, Fully.Qualified.Namespace.ColorPattern) eq Fully.Qualified.Namespace.ColorPattern'blue'")] + [InlineData("cast(null, 'Fully.Qualified.Namespace.ColorPattern') eq Fully.Qualified.Namespace.ColorPattern'blue'")] + public void CastFunctionWorksForCastFromNullToEnum(string filterQuery) { - FilterClause filter = ParseFilter("cast(null, Fully.Qualified.Namespace.ColorPattern) eq Fully.Qualified.Namespace.ColorPattern'blue'", HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet()); + FilterClause filter = ParseFilter(filterQuery, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet()); var bon = filter.Expression.ShouldBeBinaryOperatorNode(BinaryOperatorKind.Equal); var singleFunctionCallNode = bon.Left.ShouldBeSingleValueFunctionCallQueryNode("cast"); Assert.Null(Assert.IsType(singleFunctionCallNode.Parameters.ElementAt(0)).Value); - singleFunctionCallNode.Parameters.ElementAt(1).ShouldBeConstantQueryNode("Fully.Qualified.Namespace.ColorPattern"); + + string fullyQualifiedTypeName; + QueryNode secondParameterNode = singleFunctionCallNode.Parameters.ElementAt(1); + if(secondParameterNode is SingleResourceCastNode singleResourceCastNode) + { + secondParameterNode.ShouldBeSingleCastNode(HardCodedTestModel.GetColorPatternTypeReference()); + fullyQualifiedTypeName = singleResourceCastNode.TypeReference.FullName(); + } + else + { + ConstantNode constantNode = secondParameterNode.ShouldBeConstantQueryNode("Fully.Qualified.Namespace.ColorPattern"); + fullyQualifiedTypeName = constantNode.Value as string; + } + + Assert.Equal("Fully.Qualified.Namespace.ColorPattern", fullyQualifiedTypeName); + Assert.Equal("Fully.Qualified.Namespace.ColorPattern'blue'", Assert.IsType(bon.Right).LiteralText); bon.Right.ShouldBeEnumNode(HardCodedTestModel.TestModel.FindType("Fully.Qualified.Namespace.ColorPattern") as IEdmEnumType, 2L); } @@ -805,16 +1082,67 @@ public void LiteralTextShouldNeverBeNullForConstantNodeOfDottedIdentifier() Assert.Equal("Fully.Qualified.Namespace.ColorPattern'blue'", Assert.IsType(bon.Right).LiteralText); } - [Fact] - public void CastFunctionProducesAnEntityType() + [Theory] + [InlineData("cast(MyDog, 'Fully.Qualified.Namespace.Dog')/Color eq 'blue'")] + [InlineData("cast(MyDog, Fully.Qualified.Namespace.Dog)/Color eq 'blue'")] + public void CastFunctionProducesAnEntityType(string filterQuery) { - FilterClause filter = ParseFilter("cast(MyDog, 'Fully.Qualified.Namespace.Dog')/Color eq 'blue'", HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet()); + FilterClause filter = ParseFilter(filterQuery, 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()); + if(function.Parameters.ElementAt(1) is SingleResourceCastNode) + { + var singleResourceCastNode = function.Parameters.ElementAt(1).ShouldBeSingleCastNode(HardCodedTestModel.GetDogTypeReference()); + Assert.Equal("Fully.Qualified.Namespace.Dog", singleResourceCastNode.TypeReference.FullName()); + } + else + { + var constantNode = function.Parameters.ElementAt(1).ShouldBeConstantQueryNode("Fully.Qualified.Namespace.Dog"); + Assert.Equal("Fully.Qualified.Namespace.Dog", constantNode.Value as string); + } + + Assert.IsType(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(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()); - function.Parameters.ElementAt(1).ShouldBeConstantQueryNode("Fully.Qualified.Namespace.Dog"); + var singleResourceCastNode = function.Parameters.ElementAt(1).ShouldBeSingleCastNode(HardCodedTestModel.GetDogTypeReference()); + Assert.Equal("Fully.Qualified.Namespace.Dog", singleResourceCastNode.TypeReference.FullName()); Assert.IsType(filter.Expression).Right.ShouldBeConstantQueryNode("blue"); } @@ -1248,6 +1576,22 @@ public void ComputedPropertyTreatedAsOpenPropertyInOrderBy() orderByClause.Expression.ShouldBeSingleValueOpenPropertyAccessQueryNode("DoubleTotal"); } + [Fact] + public void ComputedPropertyTreatedAsOpenPropertyInCastAndOrderBy() + { + var odataQueryOptionParser = new ODataQueryOptionParser(HardCodedTestModel.TestModel, + HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet(), + new Dictionary() + { + {"$orderby", "DoubleTotal asc"}, + {"$apply", "aggregate(cast(FavoriteNumber, edm.int64) with sum as Total)/compute(Total mul 2 as DoubleTotal)"} + }) { Resolver = new ODataUriResolver() { EnableCaseInsensitive = true } }; + var applyClause = odataQueryOptionParser.ParseApply(); + var orderByClause = odataQueryOptionParser.ParseOrderBy(); + Assert.Equal(OrderByDirection.Ascending, orderByClause.Direction); + orderByClause.Expression.ShouldBeSingleValueOpenPropertyAccessQueryNode("DoubleTotal"); + } + [Fact] public void DollarComputedPropertyTreatedAsOpenPropertyInOrderBy() { @@ -3011,6 +3355,30 @@ private static FilterClause ParseFilter(string text, IEdmModel edmModel, IEdmTyp return new ODataQueryOptionParser(edmModel, edmType, edmEntitySet, new Dictionary() { { "$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() { { "$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() { { "$orderby", text } }).ParseOrderBy(); diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/UriParser/HardCodedTestModel.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/UriParser/HardCodedTestModel.cs index e52f70e281..85e70ea51c 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/UriParser/HardCodedTestModel.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/UriParser/HardCodedTestModel.cs @@ -1771,6 +1771,16 @@ public static IEdmEntityType GetFramedPaintingType() return TestModel.FindType("Fully.Qualified.Namespace.FramedPainting") as IEdmEntityType; } + public static IEdmComplexType GetColorPatternType() + { + return TestModel.FindType("Fully.Qualified.Namespace.ColorPattern") as IEdmComplexType; + } + + public static IEdmComplexTypeReference GetColorPatternTypeReference() + { + return new EdmComplexTypeReference(GetColorPatternType(), false); + } + /// /// Gets a type reference to a painting. We use 'false' for nullable because that is the value the product should set /// it to when we have to create a reference (like for the item type of the collection you are filtering or something).