diff --git a/src/Microsoft.OData.Core/UriParser/Binders/FunctionCallBinder.cs b/src/Microsoft.OData.Core/UriParser/Binders/FunctionCallBinder.cs index 066bd41cff..72a09ec99f 100644 --- a/src/Microsoft.OData.Core/UriParser/Binders/FunctionCallBinder.cs +++ b/src/Microsoft.OData.Core/UriParser/Binders/FunctionCallBinder.cs @@ -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) @@ -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); } diff --git a/src/Microsoft.OData.Core/UriParser/Parsers/FunctionCallParser.cs b/src/Microsoft.OData.Core/UriParser/Parsers/FunctionCallParser.cs index 1c5b24c4ac..96a6d23da7 100644 --- a/src/Microsoft.OData.Core/UriParser/Parsers/FunctionCallParser.cs +++ b/src/Microsoft.OData.Core/UriParser/Parsers/FunctionCallParser.cs @@ -11,6 +11,7 @@ namespace Microsoft.OData.UriParser using System.Diagnostics; using System.Linq; using Microsoft.OData.Core; + using Microsoft.OData.Edm; /// /// Implementation of IFunctionCallParser that allows functions calls and parses arguments with a provided method. @@ -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); @@ -114,8 +115,9 @@ public bool TryParseIdentifierAsFunction(QueryToken parent, out QueryToken resul /// Parses argument lists or entity key value list. /// /// Action invoked for restoring a state during failure. + /// The name of the function being called. Default is an empty span. /// The lexical tokens representing the arguments. - public FunctionParameterToken[] ParseArgumentListOrEntityKeyList(Action restoreAction = null) + public FunctionParameterToken[] ParseArgumentListOrEntityKeyList(Action restoreAction = null, ReadOnlySpan functionName = default) { if (this.Lexer.CurrentToken.Kind != ExpressionTokenKind.OpenParen) { @@ -136,7 +138,7 @@ public FunctionParameterToken[] ParseArgumentListOrEntityKeyList(Action restoreA } else { - arguments = this.ParseArguments(); + arguments = this.ParseArguments(functionName); } if (this.Lexer.CurrentToken.Kind != ExpressionTokenKind.CloseParen) @@ -161,8 +163,9 @@ 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. /// + /// The name of the function being called. Default is an empty span. /// The lexical tokens representing the arguments. - public FunctionParameterToken[] ParseArguments() + public FunctionParameterToken[] ParseArguments(ReadOnlySpan functionName = default) { ICollection argList; if (this.TryReadArgumentsAsNamedValues(out argList)) @@ -170,24 +173,43 @@ public FunctionParameterToken[] ParseArguments() return argList.ToArray(); } - return this.ReadArgumentsAsPositionalValues().ToArray(); + return this.ReadArgumentsAsPositionalValues(functionName).ToArray(); } /// /// Read the list of arguments as a set of positional values /// + /// The name of the function being called. Default is an empty span. /// A list of FunctionParameterTokens representing each argument - private List ReadArgumentsAsPositionalValues() + private List ReadArgumentsAsPositionalValues(ReadOnlySpan functionName = default) { + // Store the parent expression of the current argument. + Stack expressionParents = new Stack(); + bool isFunctionCallNameCastOrIsOf = functionName.Length > 0 && + (functionName.SequenceEqual(ExpressionConstants.UnboundFunctionCast.AsSpan()) || functionName.SequenceEqual(ExpressionConstants.UnboundFunctionIsOf.AsSpan())); List argList = new List(); 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(); } @@ -213,5 +235,35 @@ private bool TryReadArgumentsAsNamedValues(out ICollection + /// 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. + /// + /// The previous parameter token. + /// The current parameter token. + /// The updated parameter token. + 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; + } } } diff --git a/src/Microsoft.OData.Core/UriParser/Resolver/NormalizedModelElementsCache.cs b/src/Microsoft.OData.Core/UriParser/Resolver/NormalizedModelElementsCache.cs index 7d7b19f66b..41698ebc32 100644 --- a/src/Microsoft.OData.Core/UriParser/Resolver/NormalizedModelElementsCache.cs +++ b/src/Microsoft.OData.Core/UriParser/Resolver/NormalizedModelElementsCache.cs @@ -20,6 +20,11 @@ namespace Microsoft.OData.Edm /// internal sealed class NormalizedModelElementsCache { + /// + /// Provides a shared, normalized case-insensitive cache of model elements for the EDM core model. + /// + 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 we would need // to do additional work (and allocations) during lookups to filter the results to the subset that matches the request type. diff --git a/test/UnitTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/EnumFilterFunctionalTests.cs b/test/UnitTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/EnumFilterFunctionalTests.cs index fd30a3a8a5..c979439e39 100644 --- a/test/UnitTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/EnumFilterFunctionalTests.cs +++ b/test/UnitTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/EnumFilterFunctionalTests.cs @@ -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(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); @@ -500,17 +506,20 @@ public void ParseFilterEnumMemberUndefined4() parse.Throws(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(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(SRResources.CastBinder_EnumOnlyCastToOrFromString); } @@ -536,7 +545,7 @@ public void ParseFilterWithInOperatorWithEnums(string filterOptionValue) IEdmEnumType colorType = this.GetIEdmType(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'"; @@ -651,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/UnitTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/FilterAndOrderByFunctionalTests.cs b/test/UnitTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/FilterAndOrderByFunctionalTests.cs index f0bdc5a270..1cc556e1f3 100644 --- a/test/UnitTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/FilterAndOrderByFunctionalTests.cs +++ b/test/UnitTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/FilterAndOrderByFunctionalTests.cs @@ -15,6 +15,7 @@ using Microsoft.OData.UriParser; using Microsoft.Spatial; using Microsoft.Test.OData.Utils.Metadata; +using Microsoft.VisualBasic; using Xunit; namespace Microsoft.OData.Tests.ScenarioTests.UriParser @@ -760,6 +761,259 @@ 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"); + } + + [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 + } + + [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(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 + + var secondParameter = singleValueFunctionCallNode.Parameters.ElementAt(1); + string fullyQualifiedTypeName = secondParameter switch + { + SingleResourceCastNode singleResourceCastNode => secondParameter.ShouldBeSingleResourceCastNode(HardCodedTestModel.GetEmployeeTypeReference()).TypeReference.FullName(), + ConstantNode constantNode => secondParameter.ShouldBeConstantQueryNode("Fully.Qualified.Namespace.Employee").Value as string, + _ => throw new InvalidOperationException("Unexpected node type") + }; + + Assert.Equal("Fully.Qualified.Namespace.Employee", fullyQualifiedTypeName); + } + + [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 + + var secondParameter = singleValueFunctionCallNode.Parameters.ElementAt(1); + string fullyQualifiedTypeName = secondParameter switch + { + SingleResourceCastNode singleResourceCastNode => secondParameter.ShouldBeSingleResourceCastNode(HardCodedTestModel.GetHomeAddressReference()).TypeReference.FullName(), + ConstantNode constantNode => secondParameter.ShouldBeConstantQueryNode("Fully.Qualified.Namespace.HomeAddress").Value as string, + _ => throw new InvalidOperationException("Unexpected node type") + }; + + Assert.Equal("Fully.Qualified.Namespace.HomeAddress", fullyQualifiedTypeName); + } + + [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)", "'Fully.Qualified.Namespace.Pet1' is not assignable from 'Fully.Qualified.Namespace.Person'.")] + [InlineData("isof(MyAddress,Fully.Qualified.Namespace.Pet1)", "'Fully.Qualified.Namespace.Pet1' is not assignable from 'Fully.Qualified.Namespace.Address'.")] + [InlineData("isof(null,Fully.Qualified.Namespace.Person)", "'Fully.Qualified.Namespace.Person' is not assignable from ''.")] + [InlineData("isof('',Fully.Qualified.Namespace.Person)", "'Fully.Qualified.Namespace.Person' is not assignable from 'Edm.String'.")] + public void IsOfFunctionsWithUnquotedTypeParameter_WithIncorrectType_ThrowException(string filterQuery, string errorMessage) + { + // Arrange & Act + var exception = Record.Exception(() => ParseFilter(filterQuery, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet())); + + // Assert + Assert.NotNull(exception); + Assert.IsType(exception); + Assert.Equal($"Encountered invalid type cast. {errorMessage}", exception.Message); + } + + [Theory] + [InlineData("cast(Fully.Qualified.Namespace.HomeAddress)/City eq 'City1'", "'Fully.Qualified.Namespace.HomeAddress' is not assignable from 'Fully.Qualified.Namespace.Person'.")] + [InlineData("cast(MyAddress,Fully.Qualified.Namespace.Employee)/WorkID eq 345", "'Fully.Qualified.Namespace.Employee' is not assignable from 'Fully.Qualified.Namespace.Address'.")] + [InlineData("cast(null,Fully.Qualified.Namespace.Employee)/WorkID eq 345", "'Fully.Qualified.Namespace.Employee' is not assignable from ''.")] + [InlineData("cast('',Fully.Qualified.Namespace.Employee)/WorkID eq 345", "'Fully.Qualified.Namespace.Employee' is not assignable from 'Edm.String'.")] + public void CastFunctionWithUnquotedTypeParameter_WithIncorrectType_ThrowException(string filterQuery, string errorMessage) + { + // Arrange & Act + var exception = Record.Exception(() => ParseFilter(filterQuery, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet())); + + // Assert + Assert.NotNull(exception); + Assert.IsType(exception); + Assert.Equal($"Encountered invalid type cast. {errorMessage}", exception.Message); + } + + [Theory] + [InlineData("isof(Fully.Qualified.Namespace.Pet1)", "Fully.Qualified.Namespace.Pet1")] + [InlineData("cast(Fully.Qualified.Namespace.HomeAddress)/City eq 'City1'", "Fully.Qualified.Namespace.HomeAddress")] + public void IsOfAndCastFunctionsWithSingleParameterWithoutSingleQuotes_WithIncorrectType_ThrowException(string filterQuery, string fullyQualifiedTypeName) + { + // Arrange & Act + Action test = () => ParseFilter(filterQuery, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet()); + + // Assert + test.Throws(Error.Format(SRResources.MetadataBinder_HierarchyNotFollowed, fullyQualifiedTypeName, "Fully.Qualified.Namespace.Person")); + } + + [Theory] + [InlineData("isof(MyAddress,Fully.Qualified.Namespace.Pet1)", "Fully.Qualified.Namespace.Pet1")] + [InlineData("cast(MyAddress,Fully.Qualified.Namespace.Employee)/WorkID eq 345", "Fully.Qualified.Namespace.Employee")] + public void IsOfAndCastFunctionsWithTwoParameterWhereTypeParameterIsWithoutSingleQuotes_WithIncorrectType_ThrowException(string filterQuery, string fullyQualifiedTypeName) + { + // Arrange & Act + Action test = () => ParseFilter(filterQuery, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet()); + + // Assert + test.Throws(Error.Format(SRResources.MetadataBinder_HierarchyNotFollowed, fullyQualifiedTypeName, "Fully.Qualified.Namespace.Address")); + } + [Fact] public void CastFunctionWorksWithNoSingleQuotesOnType() { @@ -772,6 +1026,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 +1070,26 @@ 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"); + + QueryNode secondParameterNode = singleFunctionCallNode.Parameters.ElementAt(1); + string fullyQualifiedTypeName = secondParameterNode switch + { + SingleResourceCastNode singleResourceCastNode => singleResourceCastNode.TypeReference.FullName(), + ConstantNode constantNode => constantNode.Value as string, + _ => throw new InvalidOperationException("Unexpected node type") + }; + + 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,19 +1104,153 @@ 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()); + + var secondParameter = function.Parameters.ElementAt(1); + string fullyQualifiedTypeName = secondParameter switch + { + SingleResourceCastNode singleResourceCastNode => secondParameter.ShouldBeSingleResourceCastNode(HardCodedTestModel.GetDogTypeReference()).TypeReference.FullName(), + ConstantNode constantNode => secondParameter.ShouldBeConstantQueryNode("Fully.Qualified.Namespace.Dog").Value as string, + _ => throw new InvalidOperationException("Unexpected node type") + }; + + Assert.Equal("Fully.Qualified.Namespace.Dog", fullyQualifiedTypeName); + 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()); - 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"); } + [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(filter.Expression).Right.ShouldBeConstantQueryNode("blue"); + } + + [Theory] + [InlineData("cast(Fully.Qualified.Namespace.Employee)/WorkID eq 345")] + [InlineData("cast('Fully.Qualified.Namespace.Employee')/WorkID eq 345")] + public void CastFunctionWithOneParameter_WithOrWithoutSingleParameterProducesAnEntityType(string filterQuery) + { + // Arrange & Act + FilterClause filter = ParseFilter(filterQuery, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet()); + + // Assert + SingleResourceFunctionCallNode function = filter.Expression.ShouldBeBinaryOperatorNode(BinaryOperatorKind.Equal) + .Left.ShouldBeSingleValuePropertyAccessQueryNode(HardCodedTestModel.GetEmployeeWorkIDProperty()) + .Source.ShouldBeSingleResourceFunctionCallNode("cast"); + + Assert.Equal(2, function.Parameters.Count()); + + ResourceRangeVariableReferenceNode resourceRangeVariable = function.Parameters.ElementAt(0).ShouldBeResourceRangeVariableReferenceNode("$it"); + Assert.Equal("Fully.Qualified.Namespace.Person", resourceRangeVariable.GetEdmTypeReference().FullName()); // $it is of type Person + + QueryNode node = function.Parameters.ElementAt(1); + string fullyQualifiedTypeName = node switch + { + SingleResourceCastNode singleResourceCastNode => node.ShouldBeSingleResourceCastNode(HardCodedTestModel.GetEmployeeTypeReference()).TypeReference.FullName(), + ConstantNode constantNode => node.ShouldBeConstantQueryNode("Fully.Qualified.Namespace.Employee").Value as string, + _ => throw new InvalidOperationException("Unexpected node type") + }; + + Assert.Equal("Fully.Qualified.Namespace.Employee", fullyQualifiedTypeName); // Fully.Qualified.Namespace.Employee is the type parameter + Assert.IsType(filter.Expression).Right.ShouldBeConstantQueryNode(345); // 345 is the right operand + } + + [Theory] + [InlineData("cast(MyAddress, 'Fully.Qualified.Namespace.HomeAddress')/HomeNO eq 'H-1234'")] + [InlineData("cast(MyAddress, Fully.Qualified.Namespace.HomeAddress)/HomeNO eq 'H-1234'")] + public void CastFunctionWithTwoParameters_WithOrWithSingleParameterProducesAnEntityType(string filterQuery) + { + // Arrange & Act + FilterClause filter = ParseFilter(filterQuery, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet()); + + // Assert + SingleResourceFunctionCallNode function = filter.Expression.ShouldBeBinaryOperatorNode(BinaryOperatorKind.Equal) + .Left.ShouldBeSingleValuePropertyAccessQueryNode(HardCodedTestModel.GetAddressHomeNOProperty()) + .Source.ShouldBeSingleResourceFunctionCallNode("cast"); + Assert.Equal(2, function.Parameters.Count()); // There are two parameters + + SingleComplexNode complexNode = function.Parameters.ElementAt(0).ShouldBeSingleComplexNode(HardCodedTestModel.GetPersonAddressProp()); + Assert.Equal("Fully.Qualified.Namespace.Address", complexNode.GetEdmTypeReference().FullName()); // MyAddress is of type Address + + QueryNode node = function.Parameters.ElementAt(1); + string fullyQualifiedTypeName = node switch + { + SingleResourceCastNode singleResourceCastNode => node.ShouldBeSingleResourceCastNode(HardCodedTestModel.GetHomeAddressReference()).TypeReference.FullName(), + ConstantNode constantNode => node.ShouldBeConstantQueryNode("Fully.Qualified.Namespace.HomeAddress").Value as string, + _ => throw new InvalidOperationException("Unexpected node type") + }; + + Assert.Equal("Fully.Qualified.Namespace.HomeAddress", fullyQualifiedTypeName); // Fully.Qualified.Namespace.HomeAddress is the type parameter + Assert.IsType(filter.Expression).Right.ShouldBeConstantQueryNode("H-1234"); + } + + [Theory] + [InlineData("cast(MyAddress/AddressType, Edm.String) eq '2'")] // AddressType is Enum + [InlineData("cast(MyAddress/AddressType, 'Edm.String') eq '2'")] // AddressType is Enum + public void CastFunctionWithTwoParametersWithBuiltInTypeProducesAnEntityType(string filterQuery) + { + // Arrange + string expectedAddressType = "Fully.Qualified.Namespace.AddressType"; + + // Act + FilterClause filter = ParseFilter(filterQuery, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet()); + + // Assert + SingleValueFunctionCallNode function = filter.Expression.ShouldBeBinaryOperatorNode(BinaryOperatorKind.Equal) + .Left.ShouldBeConvertQueryNode(EdmCoreModel.Instance.GetString(true)) + .Source.ShouldBeSingleValueFunctionCallQueryNode("cast"); + + Assert.Equal(2, function.Parameters.Count()); + + SingleValuePropertyAccessNode singleValuePropertyAccessNode = function.Parameters.ElementAt(0).ShouldBeSingleValuePropertyAccessQueryNode(HardCodedTestModel.GetMyAddressAddressTypeProperty()); + Assert.Equal(expectedAddressType, singleValuePropertyAccessNode.GetEdmTypeReference().FullName()); + + function.Parameters.ElementAt(1).ShouldBeConstantQueryNode("Edm.String"); + Assert.IsType(filter.Expression).Right.ShouldBeConstantQueryNode("2"); + } + [Fact] public void OrderByWithExpression() { @@ -1299,6 +1732,23 @@ 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() { @@ -3192,5 +3642,30 @@ private static OrderByClause ParseOrderBy(string text, IEdmModel edmModel, IEdmT { return new ODataQueryOptionParser(edmModel, edmType, edmEntitySet, new Dictionary() { { "$orderby", text } }).ParseOrderBy(); } + + 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(); + } } } diff --git a/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/HardCodedTestModel.cs b/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/HardCodedTestModel.cs index fa021b4df8..d50df5b326 100644 --- a/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/HardCodedTestModel.cs +++ b/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/HardCodedTestModel.cs @@ -77,6 +77,13 @@ internal static IEdmModel GetEdmModel() NonFlagShapeType.AddMember("Triangle", new EdmEnumMemberValue(2)); NonFlagShapeType.AddMember("foursquare", new EdmEnumMemberValue(3)); model.AddElement(NonFlagShapeType); + + var addressTypeEnum = new EdmEnumType("Fully.Qualified.Namespace", "AddressType"); + addressTypeEnum.AddMember(new EdmEnumMember(addressTypeEnum, "Home", new EdmEnumMemberValue(0))); + addressTypeEnum.AddMember(new EdmEnumMember(addressTypeEnum, "Work", new EdmEnumMemberValue(1))); + addressTypeEnum.AddMember(new EdmEnumMember(addressTypeEnum, "Other", new EdmEnumMemberValue(2))); + model.AddElement(addressTypeEnum); + var addressTypeEnumReference = new EdmEnumTypeReference(addressTypeEnum, false); #endregion #region Structured Types @@ -350,6 +357,7 @@ internal static IEdmModel GetEdmModel() FullyQualifiedNamespaceAddress.AddStructuralProperty("City", EdmCoreModel.Instance.GetString(true)); FullyQualifiedNamespaceAddress.AddStructuralProperty("NextHome", FullyQualifiedNamespaceAddressTypeReference); FullyQualifiedNamespaceAddress.AddStructuralProperty("MyNeighbors", new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetString(true)))); + FullyQualifiedNamespaceAddress.AddStructuralProperty("AddressType", addressTypeEnumReference); FullyQualifiedNamespaceAddress.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo { Name = "PostBoxPainting", TargetMultiplicity = EdmMultiplicity.ZeroOrOne, Target = FullyQualifiedNamespacePainting }); model.AddElement(FullyQualifiedNamespaceAddress); @@ -978,6 +986,11 @@ internal class HardCodedTestModelXml + + + + + @@ -1180,6 +1193,7 @@ internal class HardCodedTestModelXml + @@ -1770,6 +1784,16 @@ public static IEdmEntityTypeReference GetDogTypeReference() return new EdmEntityTypeReference(GetDogType(), false); } + public static IEdmComplexType GetColorPatternType() + { + return TestModel.FindType("Fully.Qualified.Namespace.ColorPattern") as IEdmComplexType; + } + + public static IEdmComplexTypeReference GetColorPatternTypeReference() + { + return new EdmComplexTypeReference(GetColorPatternType(), false); + } + public static IEdmEntityType GetPaintingType() { return TestModel.FindType("Fully.Qualified.Namespace.Painting") as IEdmEntityType; @@ -2009,6 +2033,16 @@ public static IEdmStructuralProperty GetAddressCityProperty() return (IEdmStructuralProperty)((IEdmStructuredType)TestModel.FindType("Fully.Qualified.Namespace.Address")).FindProperty("City"); } + public static IEdmStructuralProperty GetMyAddressAddressTypeProperty() + { + return (IEdmStructuralProperty)((IEdmStructuredType)TestModel.FindType("Fully.Qualified.Namespace.Address")).FindProperty("AddressType"); + } + + public static IEdmStructuralProperty GetAddressHomeNOProperty() + { + return (IEdmStructuralProperty)((IEdmStructuredType)TestModel.FindType("Fully.Qualified.Namespace.HomeAddress")).FindProperty("HomeNO"); + } + public static IEdmStructuralProperty GetPet2PetColorPatternProperty() { return (IEdmStructuralProperty)((IEdmStructuredType)TestModel.FindType("Fully.Qualified.Namespace.Pet2")).FindProperty("PetColorPattern"); @@ -2026,6 +2060,11 @@ public static IEdmNavigationProperty GetAddressMyFavoriteNeighborNavProp() return (IEdmNavigationProperty)((IEdmStructuredType)TestModel.FindType("Fully.Qualified.Namespace.Address")).FindProperty("MyFavoriteNeighbor"); } + public static IEdmStructuralProperty GetEmployeeWorkIDProperty() + { + return (IEdmStructuralProperty)((IEdmStructuredType)TestModel.FindType("Fully.Qualified.Namespace.Employee")).FindProperty("WorkID"); + } + public static IEdmNavigationProperty GetDogFastestOwnerNavProp() { return (IEdmNavigationProperty)((IEdmStructuredType)TestModel.FindType("Fully.Qualified.Namespace.Dog")).FindProperty("FastestOwner"); diff --git a/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/Parsers/FunctionCallParserTests.cs b/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/Parsers/FunctionCallParserTests.cs index cadd95b4db..107fb0f0a7 100644 --- a/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/Parsers/FunctionCallParserTests.cs +++ b/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/Parsers/FunctionCallParserTests.cs @@ -136,6 +136,126 @@ public void FunctionCallWithOnlyOpeningParenthesis() parse.Throws(Error.Format(SRResources.UriQueryExpressionParser_ExpressionExpected, 5, "func(")); } + [Theory] + [InlineData("cast(Location, NS.HomeAddress)", "cast")] + [InlineData("isof(Location, NS.HomeAddress)", "isof")] + public void TryParseIdentifierAsFunction_CastAndIsOfFunctions_Works(string expression, string functionName) + { + // Arrange + var lexer = new ExpressionLexer(expression, true, false); + var parser = new UriQueryExpressionParser(345, lexer); + var functionCallParser = new FunctionCallParser(lexer, parser); + + // Act + bool success = functionCallParser.TryParseIdentifierAsFunction(null, out QueryToken result); + + // Assert + Assert.True(success); + Assert.NotNull(result); + Assert.Equal(QueryTokenKind.FunctionCall, result.Kind); + var functionCall = Assert.IsType(result); + Assert.Equal(functionName, functionCall.Name); + Assert.Equal(2, functionCall.Arguments.Count()); + + var secondArgumentValueToken = functionCall.Arguments.ElementAt(1).ValueToken as DottedIdentifierToken; + Assert.NotNull(secondArgumentValueToken); + Assert.Equal("NS.HomeAddress", secondArgumentValueToken.Identifier); + Assert.Equal("Location", (secondArgumentValueToken.NextToken as EndPathToken).Identifier); + } + + [Theory] + [InlineData("cast(Location, 'NS.HomeAddress')", "cast")] + [InlineData("isof(Location, 'NS.HomeAddress')", "isof")] + public void TryParseIdentifierAsFunction_CastAndIsOfFunctions_WithQuotesParameters(string expression, string functionName) + { + // Arrange + var lexer = new ExpressionLexer(expression, true, false); + var parser = new UriQueryExpressionParser(345, lexer); + var functionCallParser = new FunctionCallParser(lexer, parser); + + // Act + bool success = functionCallParser.TryParseIdentifierAsFunction(null, out QueryToken result); + + // Assert + Assert.True(success); + Assert.NotNull(result); + Assert.Equal(QueryTokenKind.FunctionCall, result.Kind); + var functionCall = Assert.IsType(result); + Assert.Equal(functionName, functionCall.Name); + Assert.Equal(2, functionCall.Arguments.Count()); + + var secondArgumentValueToken = functionCall.Arguments.ElementAt(1).ValueToken as LiteralToken; + Assert.NotNull(secondArgumentValueToken); + Assert.Equal("NS.HomeAddress", secondArgumentValueToken.Value); + + var firstArgumentEndPathToken = functionCall.Arguments.ElementAt(0).ValueToken as EndPathToken; + Assert.NotNull(firstArgumentEndPathToken); + Assert.Equal("Location", firstArgumentEndPathToken.Identifier); + } + + [Theory] + [InlineData("cast(Location, NS.HomeAddress)", "cast")] + [InlineData("isof(Location, NS.HomeAddress)", "isof")] + public void ParseArgumentListOrEntityKeyList_CastAndIsOfFunctions(string expression, string expectedFunctionName) + { + // Arrange + var lexer = new ExpressionLexer(expression, true, false); + var parser = new UriQueryExpressionParser(345, lexer); + var functionCallParser = new FunctionCallParser(lexer, parser); + + var functionName = lexer.CurrentToken.Span; + + // Move to the open paren + lexer.NextToken(); + + // Act + var arguments = functionCallParser.ParseArgumentListOrEntityKeyList(null, functionName); + + // Assert + Assert.Equal(expectedFunctionName, functionName.ToString()); + Assert.NotNull(arguments); + Assert.Equal(2, arguments.Length); + Assert.All(arguments, arg => Assert.IsType(arg)); + + var secondArgumentValueToken = arguments.ElementAt(1).ValueToken as DottedIdentifierToken; + Assert.NotNull(secondArgumentValueToken); + Assert.Equal("NS.HomeAddress", secondArgumentValueToken.Identifier); + Assert.Equal("Location", (secondArgumentValueToken.NextToken as EndPathToken).Identifier); + } + + [Theory] + [InlineData("cast(Location, 'NS.HomeAddress')", "cast")] + [InlineData("isof(Location, 'NS.HomeAddress')", "isof")] + public void ParseArgumentListOrEntityKeyList_CastAndIsOfFunctions_WithQuotesParameters(string expression, string expectedFunctionName) + { + // Arrange + var lexer = new ExpressionLexer(expression, true, false); + var parser = new UriQueryExpressionParser(345, lexer); + var functionCallParser = new FunctionCallParser(lexer, parser); + + var functionName = lexer.CurrentToken.Span; + + // Move to the open paren + lexer.NextToken(); + + // Act + var arguments = functionCallParser.ParseArgumentListOrEntityKeyList(null, functionName); + + // Assert + Assert.Equal(expectedFunctionName, functionName.ToString()); + Assert.NotNull(arguments); + Assert.Equal(2, arguments.Length); + Assert.All(arguments, arg => Assert.IsType(arg)); + + var secondArgumentValueToken = arguments.ElementAt(1).ValueToken as LiteralToken; + Assert.NotNull(secondArgumentValueToken); + Assert.Equal("NS.HomeAddress", secondArgumentValueToken.Value); + + var firstArgumentEndPathToken = arguments.ElementAt(0).ValueToken as EndPathToken; + Assert.NotNull(firstArgumentEndPathToken); + Assert.Equal("Location", firstArgumentEndPathToken.Identifier); + } + private static FunctionCallParser GetFunctionCallParser(string expression) { ExpressionLexer lexer = new ExpressionLexer(expression, true, false);