From 2cb423d3c8dc657051151e5667795f08845752b4 Mon Sep 17 00:00:00 2001 From: John Gathogo Date: Fri, 14 Nov 2025 18:17:16 +0300 Subject: [PATCH] Add support for collection parameters in URI functions --- .../SRResources.Designer.cs | 9 + src/Microsoft.OData.Core/SRResources.resx | 3 + .../Uri/ODataUriConversionUtils.cs | 18 +- src/Microsoft.OData.Core/Uri/ODataUriUtils.cs | 4 +- .../UriParser/Binders/FunctionCallBinder.cs | 141 +++-- .../UriParser/Binders/LiteralBinder.cs | 6 +- .../UriParser/Binders/MetadataBindingUtils.cs | 143 ++++- .../UriParser/Binders/ParameterAliasBinder.cs | 2 +- .../UriParser/ODataUriParser.cs | 3 +- .../SemanticAst/CollectionConstantNode.cs | 24 + .../UriParser/TypePromotionUtils.cs | 246 ++++++++- .../Binders/FunctionCallBinderTests.cs | 501 +++++++++++++----- .../Binders/MetadataBindingUtilsTests.cs | 287 +++++++++- .../UriParser/NodeAssertions.cs | 14 +- .../ODataUriFunctionsParsingTests.cs | 396 ++++++++++++++ .../CollectionConstantNodeTests.cs | 128 ++++- .../UriParser/TypePromotionUtilsTests.cs | 261 ++++++++- 17 files changed, 1967 insertions(+), 219 deletions(-) create mode 100644 test/UnitTests/Microsoft.OData.Core.Tests/UriParser/ODataUriFunctionsParsingTests.cs diff --git a/src/Microsoft.OData.Core/SRResources.Designer.cs b/src/Microsoft.OData.Core/SRResources.Designer.cs index 19d622caef..d153fb262e 100644 --- a/src/Microsoft.OData.Core/SRResources.Designer.cs +++ b/src/Microsoft.OData.Core/SRResources.Designer.cs @@ -1519,6 +1519,15 @@ internal static string MetadataBinder_FunctionArgumentNotSingleValue { } } + /// + /// Looks up a localized string similar to The argument for an invocation of a function with name '{0}' is not a single value or collection. Arguments for this function must be either a single value or collection.. + /// + internal static string MetadataBinder_FunctionArgumentNotSingleValueOrCollectionNode { + get { + return ResourceManager.GetString("MetadataBinder_FunctionArgumentNotSingleValueOrCollectionNode", resourceCulture); + } + } + /// /// Looks up a localized string similar to Encountered invalid type cast. '{0}' is not assignable from '{1}'.. /// diff --git a/src/Microsoft.OData.Core/SRResources.resx b/src/Microsoft.OData.Core/SRResources.resx index c4ba5cc8e9..764c8f43d4 100644 --- a/src/Microsoft.OData.Core/SRResources.resx +++ b/src/Microsoft.OData.Core/SRResources.resx @@ -2624,4 +2624,7 @@ The continuation task returned by the operation cannot be null. + + The argument for an invocation of a function with name '{0}' is not a single value or collection. Arguments for this function must be either a single value or collection. + \ No newline at end of file diff --git a/src/Microsoft.OData.Core/Uri/ODataUriConversionUtils.cs b/src/Microsoft.OData.Core/Uri/ODataUriConversionUtils.cs index b961a30840..a64f43b101 100644 --- a/src/Microsoft.OData.Core/Uri/ODataUriConversionUtils.cs +++ b/src/Microsoft.OData.Core/Uri/ODataUriConversionUtils.cs @@ -671,19 +671,19 @@ private static object ConvertFromResourceOrCollectionValue(string value, IEdmMod { ODataJsonPropertyAndValueDeserializer deserializer = new ODataJsonPropertyAndValueDeserializer(context); - // TODO: The way JSON array literals look in the URI is different that response payload with an array in it. + // TODO: The way JSON array literals look in the URI is different than response payload with an array in it. // The fact that we have to manually setup the underlying reader shows this different in the protocol. // There is a discussion on if we should change this or not. deserializer.JsonReader.Read(); // Move to first thing object rawResult = deserializer.ReadNonEntityValue( - null /*payloadTypeName*/, - typeReference, - null /*DuplicatePropertyNameChecker*/, - null /*CollectionWithoutExpectedTypeValidator*/, - true /*validateNullValue*/, - false /*isTopLevelPropertyValue*/, - false /*insideResourceValue*/, - null /*propertyName*/); + payloadTypeName: null, + expectedValueTypeReference: typeReference, + propertyAndAnnotationCollector: null, + collectionValidator: null, + validateNullValue: true, + isTopLevelPropertyValue: false, + insideResourceValue: false, + propertyName: null); deserializer.ReadPayloadEnd(false); return rawResult; diff --git a/src/Microsoft.OData.Core/Uri/ODataUriUtils.cs b/src/Microsoft.OData.Core/Uri/ODataUriUtils.cs index d29a5cd089..c7a51f208f 100644 --- a/src/Microsoft.OData.Core/Uri/ODataUriUtils.cs +++ b/src/Microsoft.OData.Core/Uri/ODataUriUtils.cs @@ -51,7 +51,7 @@ public static object ConvertFromUriLiteral(string value, ODataVersion version, I if (model == null) { - model = Microsoft.OData.Edm.EdmCoreModel.Instance; + model = EdmCoreModel.Instance; } // Let ExpressionLexer try to get a primitive @@ -149,7 +149,7 @@ public static string ConvertToUriLiteral(object value, ODataVersion version, IEd if (model == null) { - model = Microsoft.OData.Edm.EdmCoreModel.Instance; + model = EdmCoreModel.Instance; } ODataNullValue nullValue = value as ODataNullValue; diff --git a/src/Microsoft.OData.Core/UriParser/Binders/FunctionCallBinder.cs b/src/Microsoft.OData.Core/UriParser/Binders/FunctionCallBinder.cs index 72a09ec99f..197b71567a 100644 --- a/src/Microsoft.OData.Core/UriParser/Binders/FunctionCallBinder.cs +++ b/src/Microsoft.OData.Core/UriParser/Binders/FunctionCallBinder.cs @@ -47,45 +47,87 @@ internal FunctionCallBinder(MetadataBinder.QueryTokenVisitor bindMethod, Binding /// /// The signature to match the types to. /// The types to promote. - internal static void TypePromoteArguments(FunctionSignatureWithReturnType signature, List argumentNodes) + internal static void TypePromoteArguments( + FunctionSignatureWithReturnType signature, + (QueryNode Node, IEdmTypeReference TypeReference)[] argumentsMetadata) { // Convert all argument nodes to the best signature argument type - Debug.Assert(signature.ArgumentTypes.Length == argumentNodes.Count, "The best signature match doesn't have the same number of arguments."); - for (int i = 0; i < argumentNodes.Count; i++) + Debug.Assert(signature.ArgumentTypes.Length == argumentsMetadata.Length, + "The best signature match doesn't have the same number of arguments."); + + for (int i = 0; i < argumentsMetadata.Length; i++) { - Debug.Assert(argumentNodes[i] is SingleValueNode, "We should have already verified that all arguments are single values."); - SingleValueNode argumentNode = (SingleValueNode)argumentNodes[i]; + Debug.Assert(argumentsMetadata[i].Node is SingleValueNode or CollectionNode, + "We should have already verified that all arguments are single- or collection-valued."); IEdmTypeReference signatureArgumentType = signature.ArgumentTypes[i]; - Debug.Assert(signatureArgumentType.IsODataPrimitiveTypeKind() || signatureArgumentType.IsODataEnumTypeKind(), "Only primitive or enum types should be able to get here."); - argumentNodes[i] = MetadataBindingUtils.ConvertToTypeIfNeeded(argumentNode, signatureArgumentType); + + if (argumentsMetadata[i].Node is SingleValueNode singleValueNode) + { + if (signatureArgumentType.IsCollection() && singleValueNode is ConstantNode constantNode && constantNode.TypeReference.IsString()) + { + if (!TypePromotionUtils.TryParseCollectionConstantNode(constantNode, signatureArgumentType.AsCollection(), out CollectionConstantNode collectionConstantNode)) + { + Debug.Assert(false, "Only constant node with bracketed expression that is possible to convert to a collection that can be promoted should be able to get here."); + } + + CollectionNode collectionNode = MetadataBindingUtils.ConvertToTypeIfNeeded(collectionConstantNode, signatureArgumentType); + argumentsMetadata[i].Node = collectionNode; + argumentsMetadata[i].TypeReference = collectionNode.CollectionType; + continue; + } + + Debug.Assert(signatureArgumentType.IsODataPrimitiveTypeKind() || signatureArgumentType.IsODataEnumTypeKind(), + "Only primitive or enum types should be able to get here."); + SingleValueNode queryNode = MetadataBindingUtils.ConvertToTypeIfNeeded(singleValueNode, signatureArgumentType); + argumentsMetadata[i].Node = queryNode; + argumentsMetadata[i].TypeReference = queryNode.TypeReference; + } + else + { + Debug.Assert(signatureArgumentType is IEdmCollectionTypeReference signatureCollectionType + && (signatureCollectionType.ElementType().IsODataPrimitiveTypeKind() || signatureCollectionType.ElementType().IsODataEnumTypeKind()), + "Only primitive or enum collection element types should be able to get here."); + + CollectionNode collectionNode = MetadataBindingUtils.ConvertToTypeIfNeeded( + (CollectionNode)argumentsMetadata[i].Node, + signatureArgumentType); + argumentsMetadata[i].Node = collectionNode; + argumentsMetadata[i].TypeReference = collectionNode.CollectionType; + } } } /// - /// Checks that all arguments are SingleValueNodes + /// Checks that all arguments are single-valued or collection-valued nodes. /// /// The name of the function the arguments are from. /// The arguments to validate. - /// SingleValueNode array - internal static SingleValueNode[] ValidateArgumentsAreSingleValue(string functionName, List argumentNodes) + internal static (QueryNode Node, IEdmTypeReference TypeReference)[] ValidateArgumentNodes(string functionName, List argumentNodes) { - ExceptionUtils.CheckArgumentNotNull(functionName, "functionCallToken"); - ExceptionUtils.CheckArgumentNotNull(argumentNodes, "argumentNodes"); + ExceptionUtils.CheckArgumentNotNull(functionName, $"{nameof(functionName)}"); + ExceptionUtils.CheckArgumentNotNull(argumentNodes, $"{nameof(argumentNodes)}"); + + (QueryNode Node, IEdmTypeReference TypeReference)[] argumentsMetadata = new (QueryNode, IEdmTypeReference)[argumentNodes.Count]; - // Right now all functions take a single value for all arguments - SingleValueNode[] ret = new SingleValueNode[argumentNodes.Count]; + // Functions take a single-valued or collection-valued arguments for (int i = 0; i < argumentNodes.Count; i++) { - SingleValueNode argumentNode = argumentNodes[i] as SingleValueNode; - if (argumentNode == null) + QueryNode argumentNode = argumentNodes[i]; + if (argumentNode is SingleValueNode singleValueNode) { - throw new ODataException(Error.Format(SRResources.MetadataBinder_FunctionArgumentNotSingleValue, functionName)); + argumentsMetadata[i] = (singleValueNode, singleValueNode.TypeReference); + } + else if (argumentNode is CollectionNode collectionNode) + { + argumentsMetadata[i] = (collectionNode, collectionNode.CollectionType); + } + else + { + throw new ODataException(Error.Format(SRResources.MetadataBinder_FunctionArgumentNotSingleValueOrCollectionNode, functionName)); } - - ret[i] = argumentNode; } - return ret; + return argumentsMetadata; } /// @@ -95,24 +137,24 @@ internal static SingleValueNode[] ValidateArgumentsAreSingleValue(string functio /// The nodes of the arguments, can be new {null,null}. /// The name-signature pairs to match against /// Returns the matching signature or throws - internal static KeyValuePair MatchSignatureToUriFunction(string functionCallToken, SingleValueNode[] argumentNodes, + internal static KeyValuePair MatchSignatureToUriFunction( + string functionCallToken, + (QueryNode Node, IEdmTypeReference TypeReference)[] argumentsMetadata, IList> nameSignatures) { KeyValuePair nameSignature; - IEdmTypeReference[] argumentTypes = argumentNodes.Select(s => s.TypeReference).ToArray(); - // Handle the cases where we don't have type information (null literal, open properties) for ANY of the arguments - int argumentCount = argumentTypes.Length; - if (argumentTypes.All(a => a == null) && argumentCount > 0) + int argumentsCount = argumentsMetadata.Length; + if (argumentsMetadata.All(n => n.TypeReference == null) && argumentsCount > 0) { // we specifically want to find just the first function that matches the number of arguments, we don't care about // ambiguity here because we're already in an ambiguous case where we don't know what kind of types // those arguments are. - KeyValuePair found = nameSignatures.FirstOrDefault(pair => pair.Value.ArgumentTypes.Length == argumentCount); + KeyValuePair found = nameSignatures.FirstOrDefault(pair => pair.Value.ArgumentTypes.Length == argumentsCount); if (found.Equals(TypePromotionUtils.NotFoundKeyValuePair)) { - throw new ODataException(Error.Format(SRResources.FunctionCallBinder_CannotFindASuitableOverload, functionCallToken, argumentTypes.Length)); + throw new ODataException(Error.Format(SRResources.FunctionCallBinder_CannotFindASuitableOverload, functionCallToken, argumentsMetadata.Length)); } else { @@ -124,8 +166,7 @@ internal static KeyValuePair MatchSigna } else { - nameSignature = - TypePromotionUtils.FindBestFunctionSignature(nameSignatures, argumentNodes, functionCallToken); + nameSignature = TypePromotionUtils.FindBestFunctionSignature(functionCallToken, nameSignatures, argumentsMetadata); if (nameSignature.Equals(TypePromotionUtils.NotFoundKeyValuePair)) { throw new ODataException(Error.Format(SRResources.MetadataBinder_NoApplicableFunctionFound, @@ -303,15 +344,19 @@ private QueryNode BindAsUriFunction(FunctionCallToken functionCallToken, List> nameSignatures = GetUriFunctionSignatures(functionCallToken.Name, this.state.Configuration.EnableCaseInsensitiveUriFunctionIdentifier); - SingleValueNode[] argumentNodeArray = ValidateArgumentsAreSingleValue(functionCallToken.Name, argumentNodes); - KeyValuePair nameSignature = MatchSignatureToUriFunction(functionCallToken.Name, argumentNodeArray, nameSignatures); + (QueryNode Node, IEdmTypeReference TypeReference)[] argumentsMetadata = ValidateArgumentNodes(functionCallToken.Name, argumentNodes); + KeyValuePair nameSignature = MatchSignatureToUriFunction(functionCallToken.Name, argumentsMetadata, nameSignatures); Debug.Assert(nameSignature.Key != null, "nameSignature.Key != null"); string canonicalName = nameSignature.Key; FunctionSignatureWithReturnType signature = nameSignature.Value; if (signature != null) { - TypePromoteArguments(signature, argumentNodes); + TypePromoteArguments(signature, argumentsMetadata); + for (int i = 0; i < argumentsMetadata.Length; i++) + { + argumentNodes[i] = argumentsMetadata[i].Node; + } } if (signature.ReturnType != null && signature.ReturnType.IsStructured()) @@ -388,7 +433,7 @@ private bool TryBindIdentifier(string identifier, IEnumerable this.bindMethod(p)); + // but should be parsed into token tree, and bound to node tree: parsedParameters.Select(p => this.bindMethod(p)); ICollection parsedParameters = HandleComplexOrCollectionParameterValueIfExists(state.Configuration.Model, function, syntacticArguments, state.Configuration.Resolver.EnableCaseInsensitive); IEnumerable boundArguments = parsedParameters.Select(p => this.bindMethod(p)); @@ -430,13 +475,21 @@ private bool TryBindIdentifier(string identifier, IEnumerable /// The ODataUriParserConfiguration. - /// The function or operation. - /// The parameter tokens to be binded. - /// The binded semantic nodes. - internal static List BindSegmentParameters(ODataUriParserConfiguration configuration, IEdmOperation functionOrOpertion, ICollection segmentParameterTokens) + /// The function or operation. + /// The parameter tokens to be bound. + /// The bound semantic nodes. + internal static List BindSegmentParameters( + ODataUriParserConfiguration configuration, + IEdmOperation functionOrOperation, + ICollection segmentParameterTokens) { - // TODO: HandleComplexOrCollectionParameterValueIfExists is temp work around for single copmlex or colleciton type, it can't handle nested complex or collection value. - ICollection parametersParsed = FunctionCallBinder.HandleComplexOrCollectionParameterValueIfExists(configuration.Model, functionOrOpertion, segmentParameterTokens, configuration.Resolver.EnableCaseInsensitive, configuration.EnableUriTemplateParsing); + // TODO: HandleComplexOrCollectionParameterValueIfExists is temp work around for single complex or collection type, it can't handle nested complex or collection value. + ICollection parametersParsed = HandleComplexOrCollectionParameterValueIfExists( + configuration.Model, + functionOrOperation, + segmentParameterTokens, + configuration.Resolver.EnableCaseInsensitive, + configuration.EnableUriTemplateParsing); // Bind it to metadata BindingState state = new BindingState(configuration); @@ -445,6 +498,8 @@ internal static List BindSegmentParameters(ODataUriPa MetadataBinder binder = new MetadataBinder(state); List boundParameters = new List(); + // TODO: Why does this method only handle SingleValueNode? + // What about other QueryNode types like CollectionNode? IDictionary input = new Dictionary(StringComparer.Ordinal); foreach (FunctionParameterToken paraToken in parametersParsed) { @@ -463,7 +518,7 @@ internal static List BindSegmentParameters(ODataUriPa } } - IDictionary result = configuration.Resolver.ResolveOperationParameters(functionOrOpertion, input); + IDictionary result = configuration.Resolver.ResolveOperationParameters(functionOrOperation, input); foreach (KeyValuePair item in result) { @@ -553,7 +608,7 @@ private static bool TryRewriteIntegralConstantNode(ref SingleValueNode boundNode /// This is temp work around for $filter $orderby parameter expression which contains complex or collection /// like "Fully.Qualified.Namespace.CanMoveToAddresses(addresses=[{\"Street\":\"NE 24th St.\",\"City\":\"Redmond\"},{\"Street\":\"Pine St.\",\"City\":\"Seattle\"}])"; /// TODO: $filter $orderby parameter expression which contains nested complex or collection should NOT be supported in this way - /// but should be parsed into token tree, and binded to node tree: parsedParameters.Select(p => this.bindMethod(p)); + /// but should be parsed into token tree, and bound to node tree: parsedParameters.Select(p => this.bindMethod(p)); /// /// The model. /// IEdmFunction or IEdmOperation @@ -590,7 +645,7 @@ private static ICollection HandleComplexOrCollectionPara string valueStr = null; if (valueToken != null && (valueStr = valueToken.Value as string) != null && !string.IsNullOrEmpty(valueToken.OriginalText)) { - ExpressionLexer lexer = new ExpressionLexer(valueToken.OriginalText, true /*moveToFirstToken*/, false /*useSemicolonDelimiter*/, true /*parsingFunctionParameters*/); + ExpressionLexer lexer = new ExpressionLexer(valueToken.OriginalText, moveToFirstToken: true, useSemicolonDelimiter: false, parsingFunctionParameters: true); if (lexer.CurrentToken.Kind == ExpressionTokenKind.BracketedExpression || lexer.CurrentToken.Kind == ExpressionTokenKind.BracedExpression) { object result; @@ -608,7 +663,7 @@ private static ICollection HandleComplexOrCollectionPara } else { - // For complex & colleciton of complex directly return the raw string. + // For complex & collection of complex directly return the raw string. partiallyParsedParametersWithComplexOrCollection.Add(funcParaToken); continue; } diff --git a/src/Microsoft.OData.Core/UriParser/Binders/LiteralBinder.cs b/src/Microsoft.OData.Core/UriParser/Binders/LiteralBinder.cs index fd017c187c..1dba30d8b3 100644 --- a/src/Microsoft.OData.Core/UriParser/Binders/LiteralBinder.cs +++ b/src/Microsoft.OData.Core/UriParser/Binders/LiteralBinder.cs @@ -6,7 +6,7 @@ namespace Microsoft.OData.UriParser { - using System.Diagnostics; + using Microsoft.OData.Edm; /// /// Class that knows how to bind literal values. @@ -48,8 +48,8 @@ internal static QueryNode BindInLiteral(LiteralToken literalToken) { if (literalToken.ExpectedEdmTypeReference != null) { - OData.Edm.IEdmCollectionTypeReference collectionReference = - literalToken.ExpectedEdmTypeReference as OData.Edm.IEdmCollectionTypeReference; + IEdmCollectionTypeReference collectionReference = + literalToken.ExpectedEdmTypeReference as IEdmCollectionTypeReference; if (collectionReference != null) { ODataCollectionValue collectionValue = literalToken.Value as ODataCollectionValue; diff --git a/src/Microsoft.OData.Core/UriParser/Binders/MetadataBindingUtils.cs b/src/Microsoft.OData.Core/UriParser/Binders/MetadataBindingUtils.cs index fd7b21b6df..567f6942a0 100644 --- a/src/Microsoft.OData.Core/UriParser/Binders/MetadataBindingUtils.cs +++ b/src/Microsoft.OData.Core/UriParser/Binders/MetadataBindingUtils.cs @@ -7,6 +7,7 @@ namespace Microsoft.OData.UriParser { using System; + using System.Collections.Generic; using System.Diagnostics; using Microsoft.OData; using Microsoft.OData.Core; @@ -73,7 +74,7 @@ internal static SingleValueNode ConvertToTypeIfNeeded(SingleValueNode source, IE { if(enumType.TryParse(memberIntegralValue, out IEdmEnumMember enumMember)) { - string literalText = ODataUriUtils.ConvertToUriLiteral(enumMember.Name, default(ODataVersion)); + string literalText = ODataUriUtils.ConvertToUriLiteral(enumMember.Name, default); return new ConstantNode(new ODataEnumValue(enumMember.Name, enumType.ToString()), literalText, targetTypeReference); } @@ -82,7 +83,7 @@ internal static SingleValueNode ConvertToTypeIfNeeded(SingleValueNode source, IE string flagsValue = enumType.ParseFlagsFromIntegralValue(memberIntegralValue); if(!string.IsNullOrEmpty(flagsValue)) { - string literalText = ODataUriUtils.ConvertToUriLiteral(flagsValue, default(ODataVersion)); + string literalText = ODataUriUtils.ConvertToUriLiteral(flagsValue, default); return new ConstantNode(new ODataEnumValue(flagsValue, enumType.ToString()), literalText, targetTypeReference); } } @@ -142,8 +143,8 @@ internal static SingleValueNode ConvertToTypeIfNeeded(SingleValueNode source, IE var targetDecimalType = (IEdmDecimalTypeReference)targetTypeReference; return decimalType.Precision == targetDecimalType.Precision && decimalType.Scale == targetDecimalType.Scale ? - (SingleValueNode)candidate : - (SingleValueNode)(new ConvertNode(candidate, targetTypeReference)); + candidate : + new ConvertNode(candidate, targetTypeReference); } else { @@ -165,6 +166,140 @@ internal static SingleValueNode ConvertToTypeIfNeeded(SingleValueNode source, IE } } + /// + /// Converts a collection node's element type to when possible, + /// materializing a new for constant collections; + /// leaves non-constant/open or non-convertible collections unchanged. + /// + /// Source collection node. + /// Desired collection type (must be a collection). + /// Converted collection node or original source. + internal static CollectionNode ConvertToTypeIfNeeded(CollectionNode source, IEdmTypeReference targetTypeReference) + { + Debug.Assert(source != null, "source != null"); + + if (targetTypeReference == null) + { + return source; + } + + IEdmCollectionTypeReference sourceCollectionType = source.CollectionType; + if (sourceCollectionType == null) // Open collection? Leave as is + { + return source; + } + + if (!targetTypeReference.IsCollection()) + { + throw new ODataException(Error.Format(SRResources.MetadataBinder_CannotConvertToType, source.CollectionType.FullName(), targetTypeReference.FullName())); + } + + IEdmCollectionTypeReference targetCollectionType = targetTypeReference.AsCollection(); + + if (sourceCollectionType.IsEquivalentTo(targetCollectionType)) + { + IEdmTypeReference sourceElemType = sourceCollectionType.ElementType(); + IEdmTypeReference targetElemType = targetCollectionType.ElementType(); + if (source is CollectionConstantNode colConstantNode + && sourceElemType.IsTypeDefinition() + && targetElemType.IsPrimitive() + && sourceElemType.AsPrimitive().PrimitiveKind() == targetElemType.AsPrimitive().PrimitiveKind()) + { + List convertedNodes = ConvertNodes(colConstantNode.Collection, targetElemType); + + return new CollectionConstantNode(convertedNodes, BuildCollectionLiteral(convertedNodes, targetElemType), targetCollectionType); + } + + return source; + } + + IEdmTypeReference sourceElementType = sourceCollectionType.ElementType(); + IEdmTypeReference targetElementType = targetCollectionType.ElementType(); + + if (!TypePromotionUtils.CanConvertTo(null, sourceElementType, targetElementType)) + { + throw new ODataException(Error.Format(SRResources.MetadataBinder_CannotConvertToType, sourceElementType.FullName(), targetElementType.FullName())); + } + + if (source is CollectionConstantNode collectionConstantNode) + { + List convertedNodes = ConvertNodes(collectionConstantNode.Collection, targetElementType); + + return new CollectionConstantNode(convertedNodes, BuildCollectionLiteral(convertedNodes, targetElementType), targetCollectionType); + } + + // Non-constant collections: leave as-is (conversion implicit) + return source; + } + + /// + /// Converts each constant value to , applying enum/numeric coercion; preserves null items. + /// + /// Original constant value nodes. + /// The target primitive type. + /// List of converted constant nodes. + private static List ConvertNodes(IList nodes, IEdmTypeReference targetElementType) + { + List convertedNodes = new List(nodes.Count); + + for (int i = 0; i < nodes.Count; i++) + { + ConstantNode item = nodes[i]; + if (item == null) + { + // Preserve null + convertedNodes.Add(new ConstantNode(null, "null", targetElementType)); + continue; + } + + ConstantNode convertedNode = ConvertToTypeIfNeeded(item, targetElementType) as ConstantNode; + + // If ConvertToTypeIfNeeded returned a ConvertNode, force materialization into a ConstantNode + if (convertedNode == null) + { + // Try to keep original literal text if meaningful + string literal = item.LiteralText ?? ODataUriUtils.ConvertToUriLiteral(item.Value, ODataVersion.V4); + convertedNode = new ConstantNode(item.Value, literal, targetElementType); + } + + convertedNodes.Add(convertedNode); + } + + return convertedNodes; + } + + /// + /// Builds a bracketed collection literal (e.g. [1,2,3]) from constant nodes, quoting/escaping strings and preserving nulls. + /// + /// Constant nodes representing items. + /// Element type for string quoting rules. + /// OData collection literal text. + private static string BuildCollectionLiteral(List nodes, IEdmTypeReference typeReference) + { + Debug.Assert(typeReference != null, $"{nameof(typeReference)} != null"); + + List list = new List(); + for (int i = 0; i < nodes.Count; i++) + { + ConstantNode node = nodes[i]; + if (node == null || node.Value == null) + { + list.Add("null"); + continue; + } + + string literal = node.LiteralText ?? ODataUriUtils.ConvertToUriLiteral(node.Value, ODataVersion.V4); + if (typeReference.IsString() && !(literal.Length > 1 && literal[0] == '\'' && literal[^1] == '\'')) + { + literal = $"'{literal.Replace("'", "''", StringComparison.Ordinal)}'"; + } + + list.Add(literal); + } + + return "(" + string.Join(",", list) + ")"; + } + /// /// Retrieves type associated to a segment. /// diff --git a/src/Microsoft.OData.Core/UriParser/Binders/ParameterAliasBinder.cs b/src/Microsoft.OData.Core/UriParser/Binders/ParameterAliasBinder.cs index 9fe1b7b876..7298c89584 100644 --- a/src/Microsoft.OData.Core/UriParser/Binders/ParameterAliasBinder.cs +++ b/src/Microsoft.OData.Core/UriParser/Binders/ParameterAliasBinder.cs @@ -112,7 +112,7 @@ private static QueryToken ParseComplexOrCollectionAlias(QueryToken queryToken, I string valueStr; if (valueToken != null && (valueStr = valueToken.Value as string) != null && !string.IsNullOrEmpty(valueToken.OriginalText)) { - var lexer = new ExpressionLexer(valueToken.OriginalText, true /*moveToFirstToken*/, false /*useSemicolonDelimiter*/, true /*parsingFunctionParameters*/); + var lexer = new ExpressionLexer(valueToken.OriginalText, moveToFirstToken: true, useSemicolonDelimiter: false, parsingFunctionParameters: true); if (lexer.CurrentToken.Kind == ExpressionTokenKind.BracketedExpression || lexer.CurrentToken.Kind == ExpressionTokenKind.BracedExpression) { object result = valueStr; diff --git a/src/Microsoft.OData.Core/UriParser/ODataUriParser.cs b/src/Microsoft.OData.Core/UriParser/ODataUriParser.cs index 291fe66c80..57ed6f5a8a 100644 --- a/src/Microsoft.OData.Core/UriParser/ODataUriParser.cs +++ b/src/Microsoft.OData.Core/UriParser/ODataUriParser.cs @@ -482,12 +482,13 @@ public ODataUri ParseUri() ExceptionUtils.CheckArgumentNotNull(this.uri, "uri"); ODataPath path = this.ParsePath(); + // NOTE: ParseCompute should be called before ParseSelectAndExpand because $compute may add computed properties to the select/expand clause. + ComputeClause compute = this.ParseCompute(); SelectExpandClause selectExpand = this.ParseSelectAndExpand(); FilterClause filter = this.ParseFilter(); OrderByClause orderBy = this.ParseOrderBy(); SearchClause search = this.ParseSearch(); ApplyClause apply = this.ParseApply(); - ComputeClause compute = this.ParseCompute(); long? top = this.ParseTop(); long? skip = this.ParseSkip(); long? index = this.ParseIndex(); diff --git a/src/Microsoft.OData.Core/UriParser/SemanticAst/CollectionConstantNode.cs b/src/Microsoft.OData.Core/UriParser/SemanticAst/CollectionConstantNode.cs index b13369bf8d..77080bbc84 100644 --- a/src/Microsoft.OData.Core/UriParser/SemanticAst/CollectionConstantNode.cs +++ b/src/Microsoft.OData.Core/UriParser/SemanticAst/CollectionConstantNode.cs @@ -58,6 +58,30 @@ public CollectionConstantNode(IEnumerable objectCollection, string liter } } + /// + /// CreateS a CollectionConstantNode. + /// + /// A ConstantNode collection. + /// The literal text for this node's value, formatted according to the OData URI literal formatting rules. + /// The reference to the collection type. + /// Throws if the input literalText is null. + internal CollectionConstantNode(IEnumerable nodeCollection, string literalText, IEdmCollectionTypeReference collectionType) + { + ExceptionUtils.CheckArgumentNotNull(nodeCollection, nameof(nodeCollection)); + ExceptionUtils.CheckArgumentStringNotNullOrEmpty(literalText, nameof(literalText)); + ExceptionUtils.CheckArgumentNotNull(collectionType, nameof(collectionType)); + + this.LiteralText = literalText; + EdmCollectionType edmCollectionType = collectionType.Definition as EdmCollectionType; + this.itemType = edmCollectionType.ElementType; + this.collectionTypeReference = collectionType; + + foreach (ConstantNode item in nodeCollection) + { + this.collection.Add(item); + } + } + /// /// Gets the collection of ConstantNodes. /// diff --git a/src/Microsoft.OData.Core/UriParser/TypePromotionUtils.cs b/src/Microsoft.OData.Core/UriParser/TypePromotionUtils.cs index fae7489eb1..b33a404692 100644 --- a/src/Microsoft.OData.Core/UriParser/TypePromotionUtils.cs +++ b/src/Microsoft.OData.Core/UriParser/TypePromotionUtils.cs @@ -78,12 +78,12 @@ internal static class TypePromotionUtils /// /// Signature for add /// - private static readonly FunctionSignature[] AdditionSignatures = arithmeticSignatures.Concat(GetAdditionTermporalSignatures()).ToArray(); + private static readonly FunctionSignature[] AdditionSignatures = arithmeticSignatures.Concat(GetAdditionTemporalSignatures()).ToArray(); /// /// Signature for sub /// - private static readonly FunctionSignature[] SubtractionSignatures = arithmeticSignatures.Concat(GetSubtractionTermporalSignatures()).ToArray(); + private static readonly FunctionSignature[] SubtractionSignatures = arithmeticSignatures.Concat(GetSubtractionTemporalSignatures()).ToArray(); /// Function signatures for relational operators (eq, ne, lt, le, gt, ge). private static readonly FunctionSignature[] relationalSignatures = new FunctionSignature[] @@ -377,25 +377,26 @@ internal static bool PromoteOperandType(UnaryOperatorKind operatorKind, ref IEdm } /// Finds the best fitting function for the specified arguments. - /// Functions with names to consider. - /// Nodes of the arguments for the function, can be new {null,null}. /// Function call token used for case-sensitive matching to resolve ambiguous cases. + /// Functions with names to consider. + /// Nodes of the arguments for the function, can be new {null,null}. /// The best fitting function; null if none found or ambiguous. internal static KeyValuePair FindBestFunctionSignature( + string functionCallToken, IList> nameFunctions, - SingleValueNode[] argumentNodes, string functionCallToken) + (QueryNode Node, IEdmTypeReference TypeReference)[] argumentMetadata) { - IEdmTypeReference[] argumentTypes = argumentNodes.Select(s => s.TypeReference).ToArray(); - Debug.Assert(nameFunctions != null, "nameFunctions != null"); - Debug.Assert(argumentTypes != null, "argumentTypes != null"); - Debug.Assert(functionCallToken != null, "functionCallToken != null"); + Debug.Assert(nameFunctions != null, $"{nameof(nameFunctions)} != null"); + Debug.Assert(argumentMetadata != null, $"{nameof(argumentMetadata)} != null"); + Debug.Assert(functionCallToken != null, $"{nameof(functionCallToken)} != null"); + IList> applicableNameFunctions = new List>(nameFunctions.Count); // Build a list of applicable functions (and cache their promoted arguments). foreach (KeyValuePair candidate in nameFunctions) { - if (candidate.Value.ArgumentTypes.Length != argumentTypes.Length) + if (candidate.Value.ArgumentTypes.Length != argumentMetadata.Length) { continue; } @@ -403,7 +404,7 @@ IList> applicableNameFunct bool argumentsMatch = true; for (int i = 0; i < candidate.Value.ArgumentTypes.Length; i++) { - if (!CanPromoteNodeTo(argumentNodes[i], argumentTypes[i], candidate.Value.ArgumentTypes[i])) + if (!CanPromoteNodeTo(argumentMetadata[i].Node, argumentMetadata[i].TypeReference, candidate.Value.ArgumentTypes[i])) { argumentsMatch = false; break; @@ -428,6 +429,7 @@ IList> applicableNameFunct } else { + // TODO: Verify when this block is hit in practice and determine how it'd handle collection node arguments. // Find a single function which is better than all others. IList> equallyArgumentsMatchingNameFunctions = new List>(); @@ -436,7 +438,10 @@ IList> applicableNameFunct bool betterThanAllOthers = true; for (int j = 0; j < applicableNameFunctions.Count; j++) { - if (i != j && MatchesArgumentTypesBetterThan(argumentTypes, applicableNameFunctions[j].Value.ArgumentTypes, applicableNameFunctions[i].Value.ArgumentTypes)) + if (i != j && MatchesArgumentTypesBetterThan( + argumentMetadata.Select(d => d.TypeReference).ToArray(), + applicableNameFunctions[j].Value.ArgumentTypes, + applicableNameFunctions[i].Value.ArgumentTypes)) { betterThanAllOthers = false; break; @@ -555,7 +560,7 @@ internal static bool CanConvertTo(SingleValueNode sourceNodeOrNull, IEdmTypeRefe /// function signatures for temporal /// /// temporal function signatures for temporal for add - private static IEnumerable GetAdditionTermporalSignatures() + private static IEnumerable GetAdditionTemporalSignatures() { yield return new FunctionSignature(new[] { EdmCoreModel.Instance.GetDateTimeOffset(false), EdmCoreModel.Instance.GetDuration(false) }, new FunctionSignature.CreateArgumentTypeWithFacets[] @@ -623,7 +628,7 @@ private static IEnumerable GetAdditionTermporalSignatures() /// function signatures for temporal /// /// temporal function signatures for temporal for sub - private static IEnumerable GetSubtractionTermporalSignatures() + private static IEnumerable GetSubtractionTemporalSignatures() { yield return new FunctionSignature(new[] { EdmCoreModel.Instance.GetDateTimeOffset(false), EdmCoreModel.Instance.GetDuration(false) }, new FunctionSignature.CreateArgumentTypeWithFacets[] @@ -853,6 +858,219 @@ private static bool CanPromoteNodeTo(SingleValueNode sourceNodeOrNull, IEdmTypeR return false; } + /// Promotes the specified expression to the given type if necessary. + /// The actual argument node, may be null. + /// The actual argument type. + /// The required type to promote to. + /// True if the could be promoted; otherwise false. + private static bool CanPromoteNodeTo(CollectionNode sourceNodeOrNull, IEdmTypeReference sourceType, IEdmTypeReference targetType) + { + Debug.Assert(targetType != null, "targetType != null"); + + if (sourceType == null) + { + // Null or open collection - allow if target collection is nullable + return targetType.IsNullable; + } + + IEdmCollectionTypeReference sourceCollectionType = sourceType.AsCollectionOrNull(); + IEdmCollectionTypeReference targetCollectionType = targetType.AsCollectionOrNull(); + if (sourceCollectionType == null || targetCollectionType == null) + { + return false; + } + + IEdmTypeReference sourceElementType = sourceCollectionType.ElementType(); + IEdmTypeReference targetElementType = targetCollectionType.ElementType(); + Debug.Assert(targetElementType.IsODataPrimitiveTypeKind() || targetElementType.IsODataEnumTypeKind(), + "Type promotion only supported for primitive or enum collection types."); + + if (sourceElementType.IsEquivalentTo(targetElementType)) + { + return true; + } + + if (CanConvertTo(null, sourceElementType, targetElementType)) + { + return true; + } + + // Allow promotion from nullable to non-nullable by directly accessing underlying value. + if (sourceElementType.IsNullable && targetElementType.IsODataValueType()) + { + // COMPAT 40: Type promotion in the product allows promotion from a nullable type to arbitrary value types + IEdmTypeReference nonNullableSourceType = sourceElementType.Definition.ToTypeReference(false); + if (CanConvertTo(null, nonNullableSourceType, targetElementType)) + { + return true; + } + } + + return false; + } + + /// + /// Determines whether a syntactic or already-semantic can be promoted to (or is already compatible with) a target Edm type, + /// including support for promoting a string literal that represents a bracketed collection expression into a . + /// + /// + /// The source node. May be null (null literal / open property), + /// a , a or a . + /// + /// The current (inferred or declared) Edm type reference of the source node; may be null for null/open cases. + /// The desired Edm type reference to promote to. May be a collection or single-valued type. + /// true if the source can be used as (or promoted to) the target; false otherwise. + /// + /// This overload centralizes promotion logic for both single and collection-valued arguments used during function overload resolution. + /// It defers conversion of bracketed collection text (e.g. "[1,2,3]", "['A','B']", "[]") embedded in a until this stage + /// because at this stage the target function parameter type known. Deferring to this stage makes it possible to: + /// 1) Correctly interpret empty collections or collections whose items are all null (which are promotable to ANY collection element type), + /// 2) Avoid allocating intermediate collection node variants that may later mismatch chosen overloads, and + /// 3) Fail early for incompatible element types (e.g. "[true,false]" when the target expects a numeric collection) without guessing. + /// Workflow: + /// 1) If the target is collection-valued: + /// a) Existing instances delegate to collection element promotion. + /// b) Null source type succeed only if the target collection type is nullable. + /// c) A string whose literal text parses as an OData collection literal is heuristically converted via + /// and then validated against the target collection element type. + /// 2) If the target is single-valued, collections cannot be promoted; otherwise delegate to primitive/enum promotion rules. + public static bool CanPromoteNodeTo(QueryNode sourceNodeOrNull, IEdmTypeReference sourceType, IEdmTypeReference targetType) + { + Debug.Assert(targetType != null, "targetType != null"); + + if (targetType.IsCollection()) + { + // Case 1: Source is already a CollectionNode + if (sourceNodeOrNull is CollectionNode || (sourceType?.IsCollection() == true)) + { + return CanPromoteNodeTo(sourceNodeOrNull as CollectionNode, sourceType, targetType); + } + + // Case 2: Source is null or open-typed + if (sourceType == null) + { + return targetType.IsNullable; + } + + // Case 3: Source is a ConstantNode with LiteralText that could be a bracketed expression + if (sourceNodeOrNull is ConstantNode constantNode + && sourceType.IsString() + && !string.IsNullOrEmpty(constantNode.LiteralText)) + { + if (TryParseCollectionConstantNode(constantNode, targetType.AsCollection(), out CollectionConstantNode collectionConstantNode)) + { + return CanPromoteNodeTo( + collectionConstantNode, + collectionConstantNode.CollectionType, + targetType); + } + + return false; + } + + return false; + } + + // Non-collection target type + if (sourceNodeOrNull is CollectionNode) + { + // Cannot promote a collection to a single value + return false; + } + + return CanPromoteNodeTo(sourceNodeOrNull as SingleValueNode, sourceType, targetType); + } + + /// + /// Attempts to parse and wrap a string literal contained in a as an OData collection literal, + /// producing a semantic suitable for later promotion against a target collection type. + /// + /// The original constant node whose LiteralText may represent a bracketed collection (e.g. "[1,2,3]", "['A','B']", "[]"). + /// The function parameter or expected the caller is trying to promote to. + /// Outputs the constructed collection constant node when parsing succeeds; null otherwise. + /// true if parsing produced an and a corresponding ; + /// false if parsing fails or the literal is not a collection. + /// + /// Parsing is delegated to + /// using heuristic typing (no model supplied) to deserialize the bracketed expression. + /// If the literal represents an empty collection or all items are null, we cannot infer an element type from content; + /// the collection can be promoted to ANY element type, + /// so we construct a using the caller's . + /// If at least one non-null item exists, its CLR type is mapped to an Edm primitive type + /// and used to build the node's collection type. This enables early rejection of incompatible element scenarios + /// (e.g. string items when a numeric collection is required). + /// NOTE: + /// This transformation is intentionally performed late (during promotion) because at this stage we have the target type available to guide parsing. + /// Performing conversion early would require guessing or defaulting element types, making empty arrays ambiguous + /// and complicating overload resolution. By deferring, we align parsing outcome with the concrete expected type, simplifying validation and error messages. + /// + public static bool TryParseCollectionConstantNode(ConstantNode constantNode, IEdmCollectionTypeReference targetType, out CollectionConstantNode collectionConstantNode) + { + Debug.Assert(constantNode != null, "constantNode != null"); + Debug.Assert(targetType != null, "targetType != null"); + + collectionConstantNode = null; + + // How would we reuse the result in FunctionCallBinder.TypePromoteArguments? + try + { + object result = ODataUriUtils.ConvertFromUriLiteral( + value: constantNode.LiteralText, + version: ODataVersion.V4, + model: null, + typeReference: null); // Parsing type reference when there's no model available results into an exception. Type will be evaluated heuristically. + + if (result != null && result is ODataCollectionValue collectionValue) + { + // NOTE: Looping through all items to determine if the items are homogeneous is potentially expensive. + // We just check the first item and let an exception be thrown later if the items are not homogeneous. + object collectionItem = null; + bool collectionIsEmpty; + using (IEnumerator enumerator = collectionValue.Items.GetEnumerator()) + { + collectionIsEmpty = !enumerator.MoveNext(); + if (!collectionIsEmpty) + { + collectionItem = enumerator.Current; + } + } + + if (collectionIsEmpty || (collectionItem == null && targetType.IsNullable)) + { + // Empty collection or all items are null - can promote to any collection type + collectionConstantNode = new CollectionConstantNode(collectionValue.Items, constantNode.LiteralText, targetType.AsCollection()); + + return true; + } + + if (collectionItem == null) + { + // First item is null but the target [collection] type is not nullable - cannot promote + return false; + } + + Type collectionItemType = collectionItem.GetType(); + IEdmTypeReference collectionItemTypeReference = EdmLibraryExtensions.GetPrimitiveTypeReference(collectionItemType); + // TODO: Handle enum types + if (!collectionItemTypeReference.IsPrimitive()) + { + return false; + } + + IEdmCollectionTypeReference collectionTypeReference = new EdmCollectionTypeReference(new EdmCollectionType(collectionItemTypeReference)); + collectionConstantNode = new CollectionConstantNode(collectionValue.Items, constantNode.LiteralText, collectionTypeReference); + + return true; + } + } + catch (ODataException) + { + return false; + } + + return false; + } + /// Finds the best applicable methods from the specified array that match the arguments. /// The candidate function signatures. /// The argument types to match. diff --git a/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/Binders/FunctionCallBinderTests.cs b/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/Binders/FunctionCallBinderTests.cs index 53e8a5d3b3..262c147b92 100644 --- a/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/Binders/FunctionCallBinderTests.cs +++ b/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/Binders/FunctionCallBinderTests.cs @@ -7,10 +7,10 @@ using System; using System.Collections.Generic; using System.Linq; -using Microsoft.OData.UriParser; +using Microsoft.OData.Core; using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; using Xunit; -using Microsoft.OData.Core; namespace Microsoft.OData.Tests.UriParser.Binders { @@ -114,11 +114,12 @@ public void BindFunctionNullArgumentTypeArgumentCountMatchesFunctionSignature() public void TypePromoteArguments() { var signature = this.GetSingleSubstringFunctionSignatureForTest(); - List nodes = new List() - { - new ConstantNode("Hello"), - new ConstantNode(3) - }; + var nodes = new (QueryNode Node, IEdmTypeReference TypeReference)[] + { + (new ConstantNode("Hello"), EdmCoreModel.Instance.GetString(false)), + (new ConstantNode(3), EdmCoreModel.Instance.GetInt32(false)) + }; + FunctionCallBinder.TypePromoteArguments(signature, nodes); AssertSignatureTypesMatchArguments(signature, nodes); } @@ -127,14 +128,15 @@ public void TypePromoteArguments() public void TypePromoteArgumentsWithNullLiteralExpectConvert() { var signature = this.GetSingleSubstringFunctionSignatureForTest(); - List nodes = new List() - { - new ConstantNode(null), - new ConstantNode(3) - }; + var nodes = new (QueryNode Node, IEdmTypeReference TypeReference)[] + { + (new ConstantNode(null), null), + (new ConstantNode(3), EdmCoreModel.Instance.GetInt32(false)) + }; + FunctionCallBinder.TypePromoteArguments(signature, nodes); - nodes[0].ShouldBeConvertQueryNode(EdmPrimitiveTypeKind.String).Source.ShouldBeConstantQueryNode(null); - nodes[1].ShouldBeConstantQueryNode(3); + nodes[0].Node.ShouldBeConvertQueryNode(EdmPrimitiveTypeKind.String).Source.ShouldBeConstantQueryNode(null); + nodes[1].Node.ShouldBeConstantQueryNode(3); AssertSignatureTypesMatchArguments(signature, nodes); } @@ -143,14 +145,15 @@ public void TypePromoteArgumentsWithNullLiteralExpectConvert() public void TypePromoteAllArgumentsAreNullLiteralsExpectConvert() { var signature = this.GetSingleSubstringFunctionSignatureForTest(); - List nodes = new List() - { - new ConstantNode(null), - new ConstantNode(null) - }; + var nodes = new (QueryNode Node, IEdmTypeReference TypeReference)[] + { + (new ConstantNode(null), null), + (new ConstantNode(null), null) + }; + FunctionCallBinder.TypePromoteArguments(signature, nodes); - nodes[0].ShouldBeConvertQueryNode(EdmPrimitiveTypeKind.String).Source.ShouldBeConstantQueryNode(null); - nodes[1].ShouldBeConvertQueryNode(EdmPrimitiveTypeKind.Int32).Source.ShouldBeConstantQueryNode(null); + nodes[0].Node.ShouldBeConvertQueryNode(EdmPrimitiveTypeKind.String).Source.ShouldBeConstantQueryNode(null); + nodes[1].Node.ShouldBeConvertQueryNode(EdmPrimitiveTypeKind.Int32).Source.ShouldBeConstantQueryNode(null); AssertSignatureTypesMatchArguments(signature, nodes); } @@ -159,15 +162,16 @@ public void TypePromoteAllArgumentsAreNullLiteralsExpectConvert() public void TypePromoteArgumentsWithOpenPropertyExpectConvert() { var signature = this.GetSingleSubstringFunctionSignatureForTest(); - List nodes = new List() - { - new SingleValueOpenPropertyAccessNode(new ConstantNode(null), OpenPropertyName), - new ConstantNode(3) - }; + var nodes = new (QueryNode Node, IEdmTypeReference TypeReference)[] + { + (new SingleValueOpenPropertyAccessNode(new ConstantNode(null), OpenPropertyName), null), + (new ConstantNode(3), EdmCoreModel.Instance.GetInt32(false)) + }; + FunctionCallBinder.TypePromoteArguments(signature, nodes); - nodes[0].ShouldBeConvertQueryNode(EdmPrimitiveTypeKind.String) + nodes[0].Node.ShouldBeConvertQueryNode(EdmPrimitiveTypeKind.String) .Source.ShouldBeSingleValueOpenPropertyAccessQueryNode(OpenPropertyName); - nodes[1].ShouldBeConstantQueryNode(3); + nodes[1].Node.ShouldBeConstantQueryNode(3); AssertSignatureTypesMatchArguments(signature, nodes); } @@ -176,15 +180,16 @@ public void TypePromoteArgumentsWithOpenPropertyExpectConvert() public void TypePromoteAllArgumentsAreOpenPropertiesExpectConvert() { var signature = this.GetSingleSubstringFunctionSignatureForTest(); - List nodes = new List() - { - new SingleValueOpenPropertyAccessNode(new ConstantNode(null), OpenPropertyName), - new SingleValueOpenPropertyAccessNode(new ConstantNode(null), OpenPropertyName + "1") - }; + var nodes = new (QueryNode Node, IEdmTypeReference TypeReference)[] + { + (new SingleValueOpenPropertyAccessNode(new ConstantNode(null), OpenPropertyName), null), + (new SingleValueOpenPropertyAccessNode(new ConstantNode(null), OpenPropertyName + "1"), null) + }; + FunctionCallBinder.TypePromoteArguments(signature, nodes); - nodes[0].ShouldBeConvertQueryNode(EdmPrimitiveTypeKind.String) + nodes[0].Node.ShouldBeConvertQueryNode(EdmPrimitiveTypeKind.String) .Source.ShouldBeSingleValueOpenPropertyAccessQueryNode(OpenPropertyName); - nodes[1].ShouldBeConvertQueryNode(EdmPrimitiveTypeKind.Int32) + nodes[1].Node.ShouldBeConvertQueryNode(EdmPrimitiveTypeKind.Int32) .Source.ShouldBeSingleValueOpenPropertyAccessQueryNode(OpenPropertyName + "1"); AssertSignatureTypesMatchArguments(signature, nodes); @@ -194,15 +199,16 @@ public void TypePromoteAllArgumentsAreOpenPropertiesExpectConvert() public void TypePromoteArgumentsAreNullAndOpenPropertiesExpectConvert() { var signature = this.GetSingleSubstringFunctionSignatureForTest(); - List nodes = new List() - { - new SingleValueOpenPropertyAccessNode(new ConstantNode(null), OpenPropertyName), - new ConstantNode(null) - }; + var nodes = new (QueryNode Node, IEdmTypeReference TypeReference)[] + { + (new SingleValueOpenPropertyAccessNode(new ConstantNode(null), OpenPropertyName), null), + (new ConstantNode(null), null) + }; + FunctionCallBinder.TypePromoteArguments(signature, nodes); - nodes[0].ShouldBeConvertQueryNode(EdmPrimitiveTypeKind.String) + nodes[0].Node.ShouldBeConvertQueryNode(EdmPrimitiveTypeKind.String) .Source.ShouldBeSingleValueOpenPropertyAccessQueryNode(OpenPropertyName); - nodes[1].ShouldBeConvertQueryNode(EdmPrimitiveTypeKind.Int32) + nodes[1].Node.ShouldBeConvertQueryNode(EdmPrimitiveTypeKind.Int32) .Source.ShouldBeConstantQueryNode(null); AssertSignatureTypesMatchArguments(signature, nodes); @@ -212,11 +218,12 @@ public void TypePromoteArgumentsAreNullAndOpenPropertiesExpectConvert() public void TypePromoteArgumentsMismatchedTypes() { var signature = this.GetSingleSubstringFunctionSignatureForTest(); - List nodes = new List() - { - new ConstantNode(3), - new ConstantNode("Hello"), - }; + var nodes = new (QueryNode Node, IEdmTypeReference TypeReference)[] + { + (new ConstantNode(3), EdmCoreModel.Instance.GetInt32(false)), + (new ConstantNode("Hello"), EdmCoreModel.Instance.GetString(false) ), + }; + Action a = () => FunctionCallBinder.TypePromoteArguments(signature, nodes); a.Throws(Error.Format(SRResources.MetadataBinder_CannotConvertToType, "Edm.Int32", "Edm.String")); } @@ -225,11 +232,12 @@ public void TypePromoteArgumentsMismatchedTypes() public void TypePromoteArgumentsMismatchedTypeAndNull() { var signature = this.GetSingleSubstringFunctionSignatureForTest(); - List nodes = new List() - { - new ConstantNode(null), - new ConstantNode("Hello") - }; + var nodes = new (QueryNode Node, IEdmTypeReference TypeReference)[] + { + (new ConstantNode(null), null), + (new ConstantNode("Hello"), EdmCoreModel.Instance.GetString(false)) + }; + Action a = () => FunctionCallBinder.TypePromoteArguments(signature, nodes); a.Throws(Error.Format(SRResources.MetadataBinder_CannotConvertToType, "Edm.String", "Edm.Int32")); } @@ -239,11 +247,12 @@ public void TypePromoteArgumentsMismatchedTypeAndNull() public void TypePromoteArgumentsMismatchedTypeAndOpenProperty() { var signature = this.GetSingleSubstringFunctionSignatureForTest(); - List nodes = new List() - { - new SingleValueOpenPropertyAccessNode(new ConstantNode(null), "SomeProperty"), - new ConstantNode("Hello") - }; + var nodes = new (QueryNode Node, IEdmTypeReference TypeReference)[] + { + (new SingleValueOpenPropertyAccessNode(new ConstantNode(null), "SomeProperty"), null), + (new ConstantNode("Hello"), EdmCoreModel.Instance.GetString(false)) + }; + Action a = () => FunctionCallBinder.TypePromoteArguments(signature, nodes); a.Throws(Error.Format(SRResources.MetadataBinder_CannotConvertToType, "Edm.String", "Edm.Int32")); } @@ -258,10 +267,11 @@ public void TypePromoteEnumArguments() FunctionSignatureWithReturnType signature = new FunctionSignatureWithReturnType(EdmCoreModel.Instance.GetDouble(false), enumTypeRef); - List nodes = new List() - { - new ConstantNode("MyValue", "MyNS.MyName'MyValue'", enumTypeRef), - }; + var nodes = new (QueryNode Node, IEdmTypeReference TypeReference)[] + { + (new ConstantNode("MyValue", "MyNS.MyName'MyValue'", enumTypeRef), enumTypeRef) + }; + FunctionCallBinder.TypePromoteArguments(signature, nodes); AssertSignatureTypesMatchArguments(signature, nodes); } @@ -270,33 +280,20 @@ public void TypePromoteEnumArguments() [Fact] public void EnsureArgumentsAreSingleValue() { - List argumentNodes = - new List() - { - new ConstantNode(new DateTimeOffset(2012, 11, 19, 1, 1, 1, 1, new TimeSpan(0, 1, 1, 0))) - }; - var result = FunctionCallBinder.ValidateArgumentsAreSingleValue("year", argumentNodes); - Assert.Single(result); - Assert.Equal("Edm.DateTimeOffset", result[0].TypeReference.Definition.FullTypeName()); - } - - [Fact] - public void ShouldThrowWhenArgumentsAreNotSingleValue() - { - List argumentNodes = - new List() - { - new CollectionNavigationNode(HardCodedTestModel.GetDogsSet(), HardCodedTestModel.GetDogMyPeopleNavProp(), new EdmPathExpression("MyDog")) - }; + List argumentNodes = new List() + { + new ConstantNode(new DateTimeOffset(2012, 11, 19, 1, 1, 1, 1, new TimeSpan(0, 1, 1, 0))) + }; - Action bind = () => FunctionCallBinder.ValidateArgumentsAreSingleValue("year", argumentNodes); - bind.Throws(Error.Format(SRResources.MetadataBinder_FunctionArgumentNotSingleValue, "year")); + var result = FunctionCallBinder.ValidateArgumentNodes("year", argumentNodes); + var constantNode = Assert.IsType(Assert.Single(result).Node); + Assert.Equal("Edm.DateTimeOffset", constantNode.TypeReference.Definition.FullTypeName()); } [Fact] public void EnsureArgumentsAreSingleValueNoArguments() { - var result = FunctionCallBinder.ValidateArgumentsAreSingleValue("year", new List()); + var result = FunctionCallBinder.ValidateArgumentNodes("year", new List()); Assert.Empty(result); } @@ -304,19 +301,20 @@ public void EnsureArgumentsAreSingleValueNoArguments() [Fact] public void MatchArgumentsToSignatureDuplicateSignature() { - List argumentNodes = - new List() - { - new ConstantNode("grr" ), - new ConstantNode("grr" ) - }; var nameSignatures = this.GetDuplicateIndexOfFunctionSignatureForTest(); Action bind = () => FunctionCallBinder.MatchSignatureToUriFunction( "IndexOf", - new SingleValueNode[] { - new SingleValuePropertyAccessNode(new ConstantNode(null)/*parent*/, new EdmStructuralProperty(new EdmEntityType("MyNamespace", "MyEntityType"), "myPropertyName", argumentNodes[0].GetEdmTypeReference())), - new SingleValuePropertyAccessNode(new ConstantNode(null)/*parent*/, new EdmStructuralProperty(new EdmEntityType("MyNamespace", "MyEntityType"), "myPropertyName", argumentNodes[1].GetEdmTypeReference()))}, + new (QueryNode Node, IEdmTypeReference TypeReference)[] { + (new SingleValuePropertyAccessNode( + new ConstantNode(null)/*parent*/, + new EdmStructuralProperty(new EdmEntityType("MyNamespace", "MyEntityType"), "myPropertyName", EdmCoreModel.Instance.GetString(false))), + EdmCoreModel.Instance.GetString(false)), + (new SingleValuePropertyAccessNode( + new ConstantNode(null)/*parent*/, + new EdmStructuralProperty(new EdmEntityType("MyNamespace", "MyEntityType"), "myPropertyName", EdmCoreModel.Instance.GetString(false))), + EdmCoreModel.Instance.GetString(false)) + }, nameSignatures); bind.Throws(Error.Format(SRResources.MetadataBinder_NoApplicableFunctionFound, @@ -327,17 +325,13 @@ public void MatchArgumentsToSignatureDuplicateSignature() [Fact] public void MatchArgumentsToSignature() { - List argumentNodes = - new List() - { - new ConstantNode(new DateTimeOffset(2012, 11, 19, 1, 1, 1, 1, new TimeSpan(0, 1, 1, 0))) - }; + var argumentNodes = new (QueryNode Node, IEdmTypeReference TypeReference)[] + { + (new ConstantNode(new DateTimeOffset(2012, 11, 19, 1, 1, 1, 1, new TimeSpan(0, 1, 1, 0))), EdmCoreModel.Instance.GetDateTimeOffset(false)) + }; var signatures = this.GetHardCodedYearFunctionSignatureForTest(); - var result = FunctionCallBinder.MatchSignatureToUriFunction( - "year", - argumentNodes.Select(s => (SingleValueNode)s).ToArray(), - signatures); + var result = FunctionCallBinder.MatchSignatureToUriFunction("year", argumentNodes, signatures); Assert.Equal("Edm.Int32", result.Value.ReturnType.FullName()); Assert.Equal("Edm.DateTimeOffset", result.Value.ArgumentTypes[0].FullName()); @@ -346,16 +340,15 @@ public void MatchArgumentsToSignature() [Fact] public void MatchArgumentsToSignatureNoMatchEmpty() { - List argumentNodes = - new List() - { - new ConstantNode(4) - }; - Action bind = () => FunctionCallBinder.MatchSignatureToUriFunction( "year", - new SingleValueNode[] { - new SingleValuePropertyAccessNode(new ConstantNode(null)/*parent*/, new EdmStructuralProperty(new EdmEntityType("MyNamespace", "MyEntityType"), "myPropertyName", argumentNodes[0].GetEdmTypeReference()))}, + new (QueryNode Node, IEdmTypeReference TypeReference)[] + { + (new SingleValuePropertyAccessNode( + new ConstantNode(null)/*parent*/, + new EdmStructuralProperty(new EdmEntityType("MyNamespace", "MyEntityType"), "myPropertyName", EdmCoreModel.Instance.GetInt32(false))), + EdmCoreModel.Instance.GetInt32(false)) + }, new List>()); bind.Throws(Error.Format(SRResources.MetadataBinder_NoApplicableFunctionFound, @@ -366,16 +359,15 @@ public void MatchArgumentsToSignatureNoMatchEmpty() [Fact] public void MatchArgumentsToSignatureNoMatchContainsSignatures() { - List argumentNodes = - new List() - { - new ConstantNode(4) - }; - Action bind = () => FunctionCallBinder.MatchSignatureToUriFunction( "year", - new SingleValueNode[] { - new SingleValuePropertyAccessNode(new ConstantNode(null)/*parent*/, new EdmStructuralProperty(new EdmEntityType("MyNamespace", "MyEntityType"), "myPropertyName", argumentNodes[0].GetEdmTypeReference()))}, + new (QueryNode Node, IEdmTypeReference TypeReference)[] + { + (new SingleValuePropertyAccessNode( + new ConstantNode(null)/*parent*/, + new EdmStructuralProperty(new EdmEntityType("MyNamespace", "MyEntityType"), "myPropertyName", EdmCoreModel.Instance.GetInt32(false))), + EdmCoreModel.Instance.GetInt32(false)) + }, this.GetHardCodedYearFunctionSignatureForTest()); bind.Throws(Error.Format(SRResources.MetadataBinder_NoApplicableFunctionFound, @@ -390,9 +382,14 @@ public void MatchArgumentsToSignatureWillPickRightSignatureForSomeNullArgumentTy { var result = FunctionCallBinder.MatchSignatureToUriFunction( "substring", - new SingleValueNode[] { - new SingleValuePropertyAccessNode(new ConstantNode(null)/*parent*/, new EdmStructuralProperty(new EdmEntityType("MyNamespace", "MyEntityType"), "myPropertyName", EdmCoreModel.Instance.GetString(true))), - new SingleValueOpenPropertyAccessNode(new ConstantNode(null)/*parent*/, "myOpenPropertyname")}, // open property's TypeReference is null + new (QueryNode Node, IEdmTypeReference TypeReference)[] + { + (new SingleValuePropertyAccessNode( + new ConstantNode(null), /*parent*/ + new EdmStructuralProperty(new EdmEntityType("MyNamespace", "MyEntityType"), "myPropertyName", EdmCoreModel.Instance.GetString(true))), + EdmCoreModel.Instance.GetString(true)), + (new SingleValueOpenPropertyAccessNode(new ConstantNode(null)/*parent*/, "myOpenPropertyname"), null) // open property's TypeReference is null + }, FunctionCallBinder.GetUriFunctionSignatures("substring")); FunctionSignatureWithReturnType sig = result.Value; @@ -1396,6 +1393,268 @@ public void GetUriFunction_CanBindEnumArgument(bool explicitEnum) } } + [Fact] + public void TypePromoteArguments_CollectionPropertyAccessNode() + { + var functionSignature = new FunctionSignatureWithReturnType( + EdmCoreModel.Instance.GetDecimal(false), + EdmCoreModel.GetCollection(EdmCoreModel.Instance.GetDecimal(false))); + var collectionType = new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetInt32(false))); + var overtimeHoursProperty = new EdmStructuralProperty(new EdmEntityType("NS", "Employee"), "OvertimeHours", collectionType); + var nodes = new (QueryNode Node, IEdmTypeReference TypeReference)[] + { + (new CollectionPropertyAccessNode(new ConstantNode(null), overtimeHoursProperty), collectionType) + }; + + FunctionCallBinder.TypePromoteArguments(functionSignature, nodes); + var collectionPropertyAccessNode = nodes[0].Node.ShouldBeCollectionPropertyAccessQueryNode(overtimeHoursProperty); + // Conversion should be implicit + Assert.True(collectionPropertyAccessNode.CollectionType.IsEquivalentTo(EdmCoreModel.GetCollection(EdmCoreModel.Instance.GetInt32(false)))); + Assert.True(collectionPropertyAccessNode.ItemType.IsEquivalentTo(EdmCoreModel.Instance.GetInt32(false))); + } + + [Fact] + public void TypePromoteArguments_CollectionConstantNode() + { + var functionSignature = new FunctionSignatureWithReturnType( + EdmCoreModel.Instance.GetInt64(false), + EdmCoreModel.GetCollection(EdmCoreModel.Instance.GetInt64(false))); + var collectionType = new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetInt32(false))); + var nodes = new (QueryNode Node, IEdmTypeReference TypeReference)[] + { + (new CollectionConstantNode(new List { 1, 2, 3 }, "[1,2,3]", collectionType), collectionType) + }; + + FunctionCallBinder.TypePromoteArguments(functionSignature, nodes); + var collectionConstantNode = nodes[0].Node.ShouldBeCollectionConstantNode(EdmCoreModel.GetCollection(EdmCoreModel.Instance.GetInt64(false))); + Assert.Equal(3, collectionConstantNode.Collection.Count); + collectionConstantNode.Collection[0].ShouldBeConstantQueryNode(1L); + collectionConstantNode.Collection[1].ShouldBeConstantQueryNode(2L); + collectionConstantNode.Collection[2].ShouldBeConstantQueryNode(3L); + } + + [Fact] + public void TypePromoteArguments_ConstantNodeToCollectionConstantNode() + { + var functionSignature = new FunctionSignatureWithReturnType( + EdmCoreModel.Instance.GetInt64(false), + EdmCoreModel.GetCollection(EdmCoreModel.Instance.GetInt64(false))); + var nodes = new (QueryNode Node, IEdmTypeReference TypeReference)[] + { + (new ConstantNode("[1,2,3]", "[1,2,3]"), EdmCoreModel.Instance.GetString(false)) + }; + + FunctionCallBinder.TypePromoteArguments(functionSignature, nodes); + var collectionConstantNode = nodes[0].Node.ShouldBeCollectionConstantNode(EdmCoreModel.GetCollection(EdmCoreModel.Instance.GetInt64(false))); + Assert.Equal(3, collectionConstantNode.Collection.Count); + collectionConstantNode.Collection[0].ShouldBeConstantQueryNode(1L); + collectionConstantNode.Collection[1].ShouldBeConstantQueryNode(2L); + collectionConstantNode.Collection[2].ShouldBeConstantQueryNode(3L); + } + + [Fact] + public void MatchSignatureToUriFunction_CollectionPropertyAccessNode() + { + var functionSignature = new FunctionSignatureWithReturnType( + EdmCoreModel.Instance.GetDecimal(false), + EdmCoreModel.GetCollection(EdmCoreModel.Instance.GetDecimal(false))); + var otherFunctionSignature = new FunctionSignatureWithReturnType( + EdmCoreModel.Instance.GetInt32(false), + EdmCoreModel.Instance.GetInt32(false)); + + var collectionType = new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetInt32(false))); + var overtimeHoursProperty = new EdmStructuralProperty(new EdmEntityType("NS", "Employee"), "OvertimeHours", collectionType); + var nodes = new (QueryNode Node, IEdmTypeReference TypeReference)[] + { + (new CollectionPropertyAccessNode(new ConstantNode(null), overtimeHoursProperty), collectionType) + }; + + var result = FunctionCallBinder.MatchSignatureToUriFunction( + "func1", + nodes, + new List> + { + new KeyValuePair("func0", functionSignature), + new KeyValuePair("func1", functionSignature), + new KeyValuePair("func1", otherFunctionSignature), + new KeyValuePair("func2", otherFunctionSignature) + }); + + Assert.Equal("func1", result.Key); + Assert.Same(functionSignature, result.Value); + Assert.Equal("Edm.Decimal", result.Value.ReturnType.FullName()); + Assert.Single(result.Value.ArgumentTypes); + Assert.Equal("Collection(Edm.Decimal)", result.Value.ArgumentTypes[0].FullName()); + } + + [Fact] + public void MatchSignatureToUriFunction_CollectionPropertyAccessNode_SourceTypeMatchingTargetType() + { + var functionSignature = new FunctionSignatureWithReturnType( + EdmCoreModel.Instance.GetDecimal(false), + EdmCoreModel.GetCollection(EdmCoreModel.Instance.GetDecimal(false))); + var otherFunctionSignature = new FunctionSignatureWithReturnType( + EdmCoreModel.Instance.GetDecimal(false), + EdmCoreModel.Instance.GetDecimal(false)); + + var collectionType = new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetDecimal(false))); + var overtimeHoursProperty = new EdmStructuralProperty(new EdmEntityType("NS", "Employee"), "OvertimeHours", collectionType); + var nodes = new (QueryNode Node, IEdmTypeReference TypeReference)[] + { + (new CollectionPropertyAccessNode(new ConstantNode(null), overtimeHoursProperty), collectionType) + }; + + var result = FunctionCallBinder.MatchSignatureToUriFunction( + "func1", + nodes, + new List> + { + new KeyValuePair("func0", functionSignature), + new KeyValuePair("func1", functionSignature), + new KeyValuePair("func1", otherFunctionSignature), + new KeyValuePair("func2", otherFunctionSignature) + }); + + Assert.Equal("func1", result.Key); + Assert.Same(functionSignature, result.Value); + Assert.Equal("Edm.Decimal", result.Value.ReturnType.FullName()); + Assert.Single(result.Value.ArgumentTypes); + Assert.Equal("Collection(Edm.Decimal)", result.Value.ArgumentTypes[0].FullName()); + } + + [Fact] + public void MatchSignatureToUriFunction_CollectionConstantNode() + { + var functionSignature = new FunctionSignatureWithReturnType( + EdmCoreModel.Instance.GetInt64(false), + EdmCoreModel.GetCollection(EdmCoreModel.Instance.GetInt64(false))); + var otherFunctionSignature = new FunctionSignatureWithReturnType( + EdmCoreModel.Instance.GetInt32(false), + EdmCoreModel.Instance.GetInt32(false)); + + var collectionType = new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetInt32(false))); + var nodes = new (QueryNode Node, IEdmTypeReference TypeReference)[] + { + (new CollectionConstantNode(new List { 1, 2, 3 }, "[1,2,3]", collectionType), collectionType) + }; + + var result = FunctionCallBinder.MatchSignatureToUriFunction( + "func1", + nodes, + new List> + { + new KeyValuePair("func0", functionSignature), + new KeyValuePair("func1", functionSignature), + new KeyValuePair("func1", otherFunctionSignature), + new KeyValuePair("func2", otherFunctionSignature) + }); + + Assert.Equal("func1", result.Key); + Assert.Same(functionSignature, result.Value); + Assert.Equal("Edm.Int64", result.Value.ReturnType.FullName()); + Assert.Single(result.Value.ArgumentTypes); + Assert.Equal("Collection(Edm.Int64)", result.Value.ArgumentTypes[0].FullName()); + } + + [Fact] + public void MatchSignatureToUriFunction_ConstantNodeToCollectionConstantNode() + { + var functionSignature = new FunctionSignatureWithReturnType( + EdmCoreModel.Instance.GetInt64(false), + EdmCoreModel.GetCollection(EdmCoreModel.Instance.GetInt64(false))); + var otherFunctionSignature = new FunctionSignatureWithReturnType( + EdmCoreModel.Instance.GetInt32(false), + EdmCoreModel.Instance.GetInt32(false)); + + var nodes = new (QueryNode Node, IEdmTypeReference TypeReference)[] + { + (new ConstantNode("[1,2,3]", "[1,2,3]"), EdmCoreModel.Instance.GetString(false)) + }; + + var result = FunctionCallBinder.MatchSignatureToUriFunction( + "func1", + nodes, + new List> + { + new KeyValuePair("func0", functionSignature), + new KeyValuePair("func1", functionSignature), + new KeyValuePair("func1", otherFunctionSignature), + new KeyValuePair("func2", otherFunctionSignature) + }); + + Assert.Equal("func1", result.Key); + Assert.Same(functionSignature, result.Value); + Assert.Equal("Edm.Int64", result.Value.ReturnType.FullName()); + Assert.Single(result.Value.ArgumentTypes); + Assert.Equal("Collection(Edm.Int64)", result.Value.ArgumentTypes[0].FullName()); + } + + [Fact] + public void MatchSignatureToUriFunction_ConstantNodeToCollectionConstantNode_Empty() + { + var functionSignature = new FunctionSignatureWithReturnType( + EdmCoreModel.Instance.GetInt64(false), + EdmCoreModel.GetCollection(EdmCoreModel.Instance.GetInt64(false))); + var otherFunctionSignature = new FunctionSignatureWithReturnType( + EdmCoreModel.Instance.GetInt32(false), + EdmCoreModel.Instance.GetInt32(false)); + + var nodes = new (QueryNode Node, IEdmTypeReference TypeReference)[] + { + (new ConstantNode("[]", "[]"), EdmCoreModel.Instance.GetString(false)) + }; + + var result = FunctionCallBinder.MatchSignatureToUriFunction( + "func1", + nodes, + new List> + { + new KeyValuePair("func0", functionSignature), + new KeyValuePair("func1", functionSignature), + new KeyValuePair("func1", otherFunctionSignature), + new KeyValuePair("func2", otherFunctionSignature) + }); + + Assert.Equal("func1", result.Key); + Assert.Same(functionSignature, result.Value); + Assert.Equal("Edm.Int64", result.Value.ReturnType.FullName()); + Assert.Single(result.Value.ArgumentTypes); + Assert.Equal("Collection(Edm.Int64)", result.Value.ArgumentTypes[0].FullName()); + } + + [Fact] + public void MatchSignatureToUriFunction_ConstantNodeToCollectionConstantNode_SingleNullItem() + { + var functionSignature = new FunctionSignatureWithReturnType( + EdmCoreModel.Instance.GetInt64(false), + EdmCoreModel.GetCollection(EdmCoreModel.Instance.GetInt64(true))); + var otherFunctionSignature = new FunctionSignatureWithReturnType( + EdmCoreModel.Instance.GetInt32(false), + EdmCoreModel.Instance.GetInt32(true)); + + var nodes = new (QueryNode Node, IEdmTypeReference TypeReference)[] + { + (new ConstantNode("[null]", "[null]"), EdmCoreModel.Instance.GetString(false)) + }; + + var result = FunctionCallBinder.MatchSignatureToUriFunction( + "func1", + nodes, + new List> + { + new KeyValuePair("func0", functionSignature), + new KeyValuePair("func1", functionSignature), + new KeyValuePair("func1", otherFunctionSignature), + new KeyValuePair("func2", otherFunctionSignature) + }); + + Assert.Equal("func1", result.Key); + Assert.Same(functionSignature, result.Value); + Assert.Equal("Edm.Int64", result.Value.ReturnType.FullName()); + Assert.Single(result.Value.ArgumentTypes); + Assert.Equal("Collection(Edm.Int64)", result.Value.ArgumentTypes[0].FullName()); + } + private bool RunBindEndPathAsFunctionCall(string endPathIdentifier, out QueryNode functionCallNode) { var boundFunctionCallToken = new EndPathToken(endPathIdentifier, null); @@ -1462,11 +1721,11 @@ internal FunctionSignatureWithReturnType GetSingleSubstringFunctionSignatureForT return signatures[0]; } - private static void AssertSignatureTypesMatchArguments(FunctionSignatureWithReturnType signature, List nodes) + private static void AssertSignatureTypesMatchArguments(FunctionSignatureWithReturnType signature, (QueryNode Node, IEdmTypeReference Type)[] nodes) { for (int i = 0; i < signature.ArgumentTypes.Length; ++i) { - Assert.Same(MetadataBindingUtils.GetEdmType(nodes[i]), signature.ArgumentTypes[i].Definition); + Assert.Same(MetadataBindingUtils.GetEdmType(nodes[i].Node), signature.ArgumentTypes[i].Definition); } } diff --git a/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/Binders/MetadataBindingUtilsTests.cs b/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/Binders/MetadataBindingUtilsTests.cs index 0b2ed028ee..a1cca10535 100644 --- a/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/Binders/MetadataBindingUtilsTests.cs +++ b/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/Binders/MetadataBindingUtilsTests.cs @@ -5,10 +5,13 @@ //--------------------------------------------------------------------- using System; -using Microsoft.OData.UriParser; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.OData.Core; using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; using Xunit; -using Microsoft.OData.Core; namespace Microsoft.OData.Tests.UriParser.Binders { @@ -18,6 +21,21 @@ namespace Microsoft.OData.Tests.UriParser.Binders public class MetadataBindingUtilsTests { #region MetadataBindingUtils.ConvertToType Tests + + private static MethodInfo PrivateBuildCollectionLiteral => + typeof(MetadataBindingUtils).GetMethods(BindingFlags.NonPublic | BindingFlags.Static) + .Single(m => m.Name == "BuildCollectionLiteral"); + + private static MethodInfo PrivateConvertNodes => + typeof(MetadataBindingUtils).GetMethods(BindingFlags.NonPublic | BindingFlags.Static) + .Single( + m => m.Name == "ConvertNodes" + && m.GetParameters().Length == 2 + && m.GetParameters()[0].ParameterType == typeof(IList)); + + private static IEdmCollectionTypeReference CollectionOf(IEdmTypeReference element) => + new EdmCollectionTypeReference(new EdmCollectionType(element)); + [Fact] public void IfTypePromotionNeededConvertNodeIsCreatedAndSourcePropertySet() { @@ -214,6 +232,264 @@ public void IfTypePromotionNeeded_SourceIsFloatMemberValuesInStringAndTargetIsEn } } + [Fact] + public void ConvertCollection_TargetNull_ReturnsSource() + { + // Arrange + var elem = EdmCoreModel.Instance.GetInt32(false); + var sourceType = CollectionOf(elem); + var source = CreateCollection(new[] { new ConstantNode(1, "1", elem) }, sourceType); + + // Act + var result = MetadataBindingUtils.ConvertToTypeIfNeeded(source, null); + + // Assert + Assert.Same(source, result); + } + + [Fact] + public void ConvertCollection_TargetNotCollection_Throws() + { + // Arrange + var elem = EdmCoreModel.Instance.GetInt32(false); + var sourceType = CollectionOf(elem); + var source = CreateCollection(new[] { new ConstantNode(1, "1", elem) }, sourceType); + + // Non-collection primitive target + var nonCollectionTarget = EdmCoreModel.Instance.GetInt64(false); + + // Act + var ex = Assert.Throws(() => MetadataBindingUtils.ConvertToTypeIfNeeded(source, nonCollectionTarget)); + + // Assert + Assert.Contains(source.CollectionType.FullName(), ex.Message, StringComparison.Ordinal); + Assert.Contains(nonCollectionTarget.FullName(), ex.Message, StringComparison.Ordinal); + } + + [Fact] + public void ConvertCollection_EquivalentTypes_PrimitiveElements_ReturnsSourceUnchanged() + { + // Arrange + var elem = EdmCoreModel.Instance.GetString(true); + var type = CollectionOf(elem); + var source = CreateCollection(new[] { new ConstantNode("a", "'a'", elem) }, type); + // Act + var result = MetadataBindingUtils.ConvertToTypeIfNeeded(source, type); + + // Assert + Assert.Same(source, result); + } + + [Fact] + public void ConvertCollection_EquivalentTypes_TypeDefinitionElements_ConvertsUnderlying() + { + // Arrange + // Define a type definition over Int32 + var intDef = new EdmTypeDefinition("NS", "MyIntDef", EdmPrimitiveTypeKind.Int32); + var sourceElemDef = new EdmTypeDefinitionReference(intDef, false); + var targetElem = EdmCoreModel.Instance.GetInt32(false); + + var sourceType = CollectionOf(sourceElemDef); + var targetType = CollectionOf(targetElem); + + Assert.True(sourceType.IsEquivalentTo(targetType)); // Equivalent collection types + + var source = CreateCollection(new[] + { + new ConstantNode(10, "10", sourceElemDef), + new ConstantNode(20, "20", sourceElemDef) + }, sourceType); + + // Act + var resultNode = MetadataBindingUtils.ConvertToTypeIfNeeded(source, targetType); + + // Assert + Assert.NotSame(source, resultNode); // New collection produced due to definition conversion + + var collectionConstant = Assert.IsType(resultNode); + Assert.Equal(2, collectionConstant.Collection.Count); + Assert.All(collectionConstant.Collection, c => + { + Assert.Equal(EdmPrimitiveTypeKind.Int32, c.TypeReference.AsPrimitive().PrimitiveKind()); + }); + + // Literal rebuilt via BuildCollectionLiteral + Assert.Equal("(10,20)", collectionConstant.LiteralText); + } + + [Fact] + public void ConvertCollection_NonEquivalentConvertible_PrimitiveElements_ProducesNewCollection() + { + // Arrange + var sourceElem = EdmCoreModel.Instance.GetInt32(true); + var targetElem = EdmCoreModel.Instance.GetDecimal(false); + + var sourceType = CollectionOf(sourceElem); + var targetType = CollectionOf(targetElem); + + var source = CreateCollection(new[] + { + new ConstantNode(1, "1", sourceElem), + new ConstantNode(2, "2", sourceElem) + }, sourceType); + + // Act + var result = MetadataBindingUtils.ConvertToTypeIfNeeded(source, targetType); + + // Assert + Assert.NotSame(source, result); + + var converted = Assert.IsType(result); + Assert.Equal(2, converted.Collection.Count); + Assert.All(converted.Collection, c => + { + Assert.Equal(EdmPrimitiveTypeKind.Decimal, c.TypeReference.AsPrimitive().PrimitiveKind()); + }); + Assert.Equal("(1,2)", converted.LiteralText); + } + + [Fact] + public void ConvertCollection_ItemProducesConvertNode_MaterializedAsConstant() + { + // Arrange + var sourceElem = EdmCoreModel.Instance.GetInt32(false); + var targetElem = EdmCoreModel.Instance.GetDecimal(false); + var sourceType = CollectionOf(sourceElem); + var targetType = CollectionOf(targetElem); + + var item = new ConstantNode(123, "123", sourceElem); + var source = CreateCollection(new[] { item }, sourceType); + + // Act + var result = MetadataBindingUtils.ConvertToTypeIfNeeded(source, targetType); + + // Assert + var converted = Assert.IsType(result); + var convertedItem = converted.Collection.Single(); + + Assert.Equal(123m, convertedItem.Value); + Assert.Equal(EdmPrimitiveTypeKind.Decimal, convertedItem.TypeReference.AsPrimitive().PrimitiveKind()); + Assert.Equal("(123)", converted.LiteralText); + } + + [Fact] + public void ConvertCollection_NonConvertibleElementTypes_Throws() + { + // Arrange + var sourceElem = EdmCoreModel.Instance.GetString(false); + var targetElem = EdmCoreModel.Instance.GetDate(false); // string -> date not directly convertible in promotion rules + var sourceType = CollectionOf(sourceElem); + var targetType = CollectionOf(targetElem); + + var source = CreateCollection(new[] { new ConstantNode("2020-01-01", "'2020-01-01'", sourceElem) }, sourceType); + + // Act + var ex = Assert.Throws(() => MetadataBindingUtils.ConvertToTypeIfNeeded(source, targetType)); + + // Assert + Assert.Contains(sourceElem.FullName(), ex.Message, StringComparison.Ordinal); + Assert.Contains(targetElem.FullName(), ex.Message, StringComparison.Ordinal); + } + + [Fact] + public void ConvertNodes_PreservesNullAndCoercesTypes() + { + // Arrange + var sourceElem = EdmCoreModel.Instance.GetInt32(true); + var targetElem = EdmCoreModel.Instance.GetInt64(false); + + var list = new List + { + null, // should become ConstantNode(null,"null",targetElem) + new ConstantNode(9, "9", sourceElem) + }; + + // Act + var converted = (List)PrivateConvertNodes.Invoke( + null, + new object[] { list, targetElem }); + + + // Assert + Assert.Equal(2, converted.Count); + Assert.Null(converted[0].Value); + Assert.Equal("null", converted[0].LiteralText); + Assert.Equal(EdmPrimitiveTypeKind.Int64, converted[1].TypeReference.AsPrimitive().PrimitiveKind()); + } + + [Fact] + public void BuildCollectionLiteral_QuotesStringsAndEscapes() + { + // Arrange + var stringType = EdmCoreModel.Instance.GetString(false); + var nodes = new List + { + new ConstantNode("abc", "abc", stringType), // needs quoting + new ConstantNode("O'Malley", "O'Malley", stringType), // needs quote + escape + new ConstantNode("'alreadyQuoted'", "'alreadyQuoted'", stringType), // already quoted + new ConstantNode(null, "null", stringType), // null value -> null literal + }; + + // Act + string literal = (string)PrivateBuildCollectionLiteral.Invoke(null, new object[] { nodes, stringType }); + + // Assert + Assert.Equal("('abc','O''Malley','alreadyQuoted',null)", literal); + } + + [Fact] + public void BuildCollectionLiteral_NonString_NoQuoting() + { + // Arrange + var intType = EdmCoreModel.Instance.GetInt32(true); + var nodes = new List + { + new ConstantNode(1, "1", intType), + new ConstantNode(2, "2", intType), + new ConstantNode(null, "null", intType), + }; + + // Act + string literal = (string)PrivateBuildCollectionLiteral.Invoke(null, new object[] { nodes, intType }); + + // Assert + Assert.Equal("(1,2,null)", literal); + } + + [Fact] + public void ConvertCollection_TypeDefinitionWithMixedItems_ProducesEscapedLiteral() + { + // Arrange + var def = new EdmTypeDefinition("NS", "MyStringDef", EdmPrimitiveTypeKind.String); + var sourceElemDef = new EdmTypeDefinitionReference(def, true); + var targetElem = EdmCoreModel.Instance.GetString(true); + + var sourceType = CollectionOf(sourceElemDef); + var targetType = CollectionOf(targetElem); + + var source = CreateCollection(new[] + { + new ConstantNode("plain", "plain", sourceElemDef), + new ConstantNode("O'Hara", "O'Hara", sourceElemDef), + new ConstantNode("'quotedAlready'", "'quotedAlready'", sourceElemDef), + new ConstantNode(null, "null", sourceElemDef) + }, sourceType); + + // Act + var result = MetadataBindingUtils.ConvertToTypeIfNeeded(source, targetType); + + // Assert + var converted = Assert.IsType(result); + Assert.Equal("('plain','O''Hara','quotedAlready',null)", converted.LiteralText); + Assert.All(converted.Collection, n => + { + if (n.Value != null) + { + Assert.Equal(EdmPrimitiveTypeKind.String, n.TypeReference.AsPrimitive().PrimitiveKind()); + } + }); + } + [Theory] [InlineData("FullTime")] [InlineData("PartTime")] @@ -323,6 +599,13 @@ private static EdmEnumType EmployeeType } } + private static CollectionConstantNode CreateCollection(IEnumerable items, IEdmCollectionTypeReference typeRef, string literal = null) + { + var list = items?.ToList() ?? new List(); + literal ??= "[" + string.Join(",", list.Select(n => n?.LiteralText ?? (n?.Value == null ? "null" : ODataUriUtils.ConvertToUriLiteral(n.Value, ODataVersion.V4)))) + "]"; + return new CollectionConstantNode(list, literal, typeRef); + } + private static EdmEnumType EmployeeTypeWithFlags { get diff --git a/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/NodeAssertions.cs b/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/NodeAssertions.cs index 14344aafd4..e2cfec693a 100644 --- a/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/NodeAssertions.cs +++ b/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/NodeAssertions.cs @@ -195,7 +195,7 @@ public static CollectionResourceFunctionCallNode ShouldBeCollectionResourceFunct Assert.NotNull(node); var functionCallNode = Assert.IsType(node); functionCallNode.Functions.ContainExactly(operationImports); - return functionCallNode; + return functionCallNode; } public static SingleValuePropertyAccessNode ShouldBeSingleValuePropertyAccessQueryNode(this QueryNode node, IEdmProperty expectedProperty) @@ -238,7 +238,7 @@ public static CountNode ShouldBeCountNode(this QueryNode node) var propertyAccessNode = Assert.IsType(node); return propertyAccessNode; } - + public static CollectionOpenPropertyAccessNode ShouldBeCollectionOpenPropertyAccessQueryNode(this QueryNode node, string expectedPropertyName) { Assert.NotNull(node); @@ -374,7 +374,7 @@ public static ConstantNode ShouldBeStringCompatibleEnumNode(this QueryNode node, Assert.Equal(enumType.FullTypeName(), enumNode.TypeReference.FullName()); Assert.Equal(value, ((ODataEnumValue)enumNode.Value).Value); - + return enumNode; } @@ -426,5 +426,13 @@ public static UriTemplateExpression ShouldBeUriTemplateExpression(this object no Assert.True(uriTemplate.ExpectedType.IsEquivalentTo(expectedType)); return uriTemplate; } + + public static CollectionConstantNode ShouldBeCollectionConstantNode(this QueryNode node, IEdmCollectionTypeReference expectedCollectionTypeReference) + { + Assert.NotNull(node); + var collectionConstantNode = Assert.IsType(node); + Assert.True(collectionConstantNode.CollectionType.IsEquivalentTo(expectedCollectionTypeReference)); + return collectionConstantNode; + } } } diff --git a/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/ODataUriFunctionsParsingTests.cs b/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/ODataUriFunctionsParsingTests.cs new file mode 100644 index 0000000000..982e1609f0 --- /dev/null +++ b/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/ODataUriFunctionsParsingTests.cs @@ -0,0 +1,396 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +using System; +using System.Linq; +using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; +using Xunit; + +namespace Microsoft.OData.Tests.UriParser +{ + /// + /// Unit tests for Uri functions parsing tests. + /// + public class ODataUriFunctionsParsingTests : IDisposable + { + private const string CalculateBenefitFunctionName = "calculateBenefit"; + private const string CalculateTaxablePayFunctionName = "calculateTaxablePay"; + private const string CalculateAdjustedSalaryFunctionName = "calculateAdjustedSalary"; + private const string CalculateSalaryBandFunctionName = "calculateSalaryBand"; + + private EdmModel model; + + public ODataUriFunctionsParsingTests() + { + InitializeModel(); + + // Function with single-valued parameter + CustomUriFunctions.AddCustomUriFunction( + CalculateBenefitFunctionName, + new FunctionSignatureWithReturnType( + EdmCoreModel.Instance.GetDecimal(false), + EdmCoreModel.Instance.GetDecimal(false))); + // Function with collection-valued parameter + CustomUriFunctions.AddCustomUriFunction( + CalculateTaxablePayFunctionName, + new FunctionSignatureWithReturnType( + EdmCoreModel.Instance.GetDecimal(false), + EdmCoreModel.Instance.GetDecimal(false), + EdmCoreModel.GetCollection(EdmCoreModel.Instance.GetDecimal(false)))); + // Function with single-valued literal parameter + CustomUriFunctions.AddCustomUriFunction( + CalculateAdjustedSalaryFunctionName, + new FunctionSignatureWithReturnType( + EdmCoreModel.Instance.GetDecimal(false), + EdmCoreModel.Instance.GetDecimal(false), + EdmCoreModel.Instance.GetDecimal(false))); + // Function with collection-valued literal parameter + CustomUriFunctions.AddCustomUriFunction( + CalculateSalaryBandFunctionName, + new FunctionSignatureWithReturnType( + EdmCoreModel.Instance.GetDecimal(false), + EdmCoreModel.Instance.GetDecimal(false), + EdmCoreModel.GetCollection(EdmCoreModel.Instance.GetDecimal(false)))); + } + + public void Dispose() + { + CustomUriFunctions.RemoveCustomUriFunction(CalculateBenefitFunctionName); + CustomUriFunctions.RemoveCustomUriFunction(CalculateTaxablePayFunctionName); + CustomUriFunctions.RemoveCustomUriFunction(CalculateAdjustedSalaryFunctionName); + CustomUriFunctions.RemoveCustomUriFunction(CalculateSalaryBandFunctionName); + } + + [Fact] + public void ParseUri_WithEdmFunctionInCompute_ParsesSelectAndComputeSuccessfully() + { + // Arrange + var odataUriParser = new ODataUriParser( + this.model, + new Uri("http://tempuri.org"), + new Uri($"http://tempuri.org/Employees(1)?$compute=Default.CalculateBonus(salary=Salary)%20as%20Bonus&$select=Name,Salary,Bonus")); + + // Act + var parsedResult = odataUriParser.ParseUri(); + + // Assert + Assert.NotNull(parsedResult); + Assert.NotNull(parsedResult.Compute); + var computedItems = parsedResult.Compute.ComputedItems.ToList(); + Assert.Single(computedItems); + var bonusItem = computedItems[0]; + Assert.Equal("Bonus", bonusItem.Alias); + var funcCall = Assert.IsType(bonusItem.Expression); + Assert.Equal("Default.CalculateBonus", funcCall.Name); + var parameters = funcCall.Parameters.ToList(); + Assert.Single(parameters); + var salaryParameter = Assert.IsType(parameters[0]); + var salaryParameterValue = Assert.IsType(salaryParameter.Value); + Assert.Equal("Salary", salaryParameterValue.Property.Name); + Assert.NotNull(parsedResult.SelectAndExpand); + var selectedProps = parsedResult.SelectAndExpand.SelectedItems.OfType().Select(i => i.SelectedPath.LastSegment.Identifier).ToList(); + Assert.Contains("Name", selectedProps); + Assert.Contains("Salary", selectedProps); + Assert.Contains("Bonus", selectedProps); + } + + [Fact] + public void ParseUri_WithEdmFunctionInCompute_CollectionValuedParameter_ParsesSelectAndComputeSuccessfully() + { + // Arrange + var odataUriParser = new ODataUriParser( + this.model, + new Uri("http://tempuri.org"), + new Uri($"http://tempuri.org/Employees(1)?$compute=Default.CalculateOvertimePay(salary=Salary,overtimeHours=OvertimeHours)%20as%20OvertimePay&$select=Name,Salary,OvertimePay")); + + // Act + var parsedResult = odataUriParser.ParseUri(); + + // Assert + Assert.NotNull(parsedResult); + Assert.NotNull(parsedResult.Compute); + var computedItems = parsedResult.Compute.ComputedItems.ToList(); + Assert.Single(computedItems); + var overtimePayItem = computedItems[0]; + Assert.Equal("OvertimePay", overtimePayItem.Alias); + var funcCall = Assert.IsType(overtimePayItem.Expression); + Assert.Equal("Default.CalculateOvertimePay", funcCall.Name); + var parameters = funcCall.Parameters.ToList(); + Assert.Equal(2, parameters.Count); + var salaryParameter = Assert.IsType(parameters[0]); + var overtimeHoursParameter = Assert.IsType(parameters[1]); + var salaryParameterValue = Assert.IsType(salaryParameter.Value); + var overtimeHoursParameterValue = Assert.IsType(overtimeHoursParameter.Value); + Assert.Equal("Salary", salaryParameterValue.Property.Name); + Assert.Equal("OvertimeHours", overtimeHoursParameterValue.Property.Name); + Assert.NotNull(parsedResult.SelectAndExpand); + var selectedProps = parsedResult.SelectAndExpand.SelectedItems.OfType().Select(i => i.SelectedPath.LastSegment.Identifier).ToList(); + Assert.Contains("Name", selectedProps); + Assert.Contains("Salary", selectedProps); + Assert.Contains("OvertimePay", selectedProps); + } + + [Fact] + public void ParseUri_WithEdmFunctionInCompute_SingleValuedLiteral_ParsesSelectAndComputeSuccessfully() + { + // Arrange + var odataUriParser = new ODataUriParser( + this.model, + new Uri("http://tempuri.org"), + new Uri($"http://tempuri.org/Employees(1)?$compute=Default.GetScore(maxScore=100)%20as%20Score&$select=Name,Score")); + + // Act + var parsedResult = odataUriParser.ParseUri(); + + // Assert + Assert.NotNull(parsedResult); + Assert.NotNull(parsedResult.Compute); + var computedItems = parsedResult.Compute.ComputedItems.ToList(); + Assert.Single(computedItems); + var scoreItem = computedItems[0]; + Assert.Equal("Score", scoreItem.Alias); + var funcCall = Assert.IsType(scoreItem.Expression); + Assert.Equal("Default.GetScore", funcCall.Name); + var parameters = funcCall.Parameters.ToList(); + Assert.Single(parameters); + var maxScoreParameter = Assert.IsType(parameters[0]); + var maxScoreLiteral = Assert.IsType(maxScoreParameter.Value); + Assert.Equal(100, maxScoreLiteral.Value); + Assert.NotNull(parsedResult.SelectAndExpand); + var selectedProps = parsedResult.SelectAndExpand.SelectedItems.OfType().Select(i => i.SelectedPath.LastSegment.Identifier).ToList(); + Assert.Contains("Name", selectedProps); + Assert.Contains("Score", selectedProps); + } + + [Fact] + public void ParseUri_WithEdmFunctionInCompute_CollectionValuedLiteral_ParsesSelectAndComputeSuccessfully() + { + // Arrange + var odataUriParser = new ODataUriParser( + this.model, + new Uri("http://tempuri.org"), + new Uri($"http://tempuri.org/Employees(1)?$compute=Default.GetRating(scale=[1,2,3,4,5])%20as%20Rating&$select=Name,Rating")); + + // Act + var parsedResult = odataUriParser.ParseUri(); + + // Assert + Assert.NotNull(parsedResult); + Assert.NotNull(parsedResult.Compute); + var computedItems = parsedResult.Compute.ComputedItems.ToList(); + Assert.Single(computedItems); + var ratingItem = computedItems[0]; + Assert.Equal("Rating", ratingItem.Alias); + var funcCall = Assert.IsType(ratingItem.Expression); + Assert.Equal("Default.GetRating", funcCall.Name); + var parameters = funcCall.Parameters.ToList(); + Assert.Single(parameters); + var scaleParameter = Assert.IsType(parameters[0]); + var scaleLiteral = Assert.IsType(scaleParameter.Value); + Assert.Equal("[1,2,3,4,5]", scaleLiteral.LiteralText); + Assert.NotNull(parsedResult.SelectAndExpand); + var selectedProps = parsedResult.SelectAndExpand.SelectedItems.OfType().Select(i => i.SelectedPath.LastSegment.Identifier).ToList(); + Assert.Contains("Name", selectedProps); + Assert.Contains("Rating", selectedProps); + } + + [Fact] + public void ParseUri_WithCustomFunctionInCompute_ParsesSelectAndComputeSuccessfully() + { + // Arrange + var odataUriParser = new ODataUriParser( + this.model, + new Uri("http://tempuri.org"), + new Uri($"http://tempuri.org/Employees?$compute=calculateBenefit(Salary)%20as%20Benefit&$select=Name,Salary,Benefit")); + + // Act + var parsedResult = odataUriParser.ParseUri(); + + // Assert + Assert.NotNull(parsedResult); + Assert.NotNull(parsedResult.Compute); + var computedItems = parsedResult.Compute.ComputedItems.ToList(); + Assert.Single(computedItems); + var benefitItem = computedItems[0]; + Assert.Equal("Benefit", benefitItem.Alias); + var funcCall = Assert.IsType(benefitItem.Expression); + Assert.Equal("calculateBenefit", funcCall.Name); + var parameters = funcCall.Parameters.ToList(); + Assert.Single(parameters); + var salaryParameter = Assert.IsType(parameters[0]); + Assert.Equal("Salary", salaryParameter.Property.Name); + var selectedProps = parsedResult.SelectAndExpand.SelectedItems.OfType().Select(i => i.SelectedPath.LastSegment.Identifier).ToList(); + Assert.Contains("Name", selectedProps); + Assert.Contains("Salary", selectedProps); + Assert.Contains("Benefit", selectedProps); + } + + [Fact] + public void ParseUri_WithCustomFunctionInCompute_CollectionValuedParameter_ParsesSelectAndComputeSuccessfully() + { + // Arrange + var odataUriParser = new ODataUriParser( + this.model, + new Uri("http://tempuri.org"), + new Uri($"http://tempuri.org/Employees?$compute=calculateTaxablePay(Salary,OvertimeHours)%20as%20TaxablePay&$select=Name,Salary,TaxablePay")); + + // Act + var parsedResult = odataUriParser.ParseUri(); + + // Assert + Assert.NotNull(parsedResult); + Assert.NotNull(parsedResult.Compute); + var computedItems = parsedResult.Compute.ComputedItems.ToList(); + Assert.Single(computedItems); + var taxablePayItem = computedItems[0]; + Assert.Equal("TaxablePay", taxablePayItem.Alias); + var funcCall = Assert.IsType(taxablePayItem.Expression); + Assert.Equal("calculateTaxablePay", funcCall.Name); + var parameters = funcCall.Parameters.ToList(); + Assert.Equal(2, parameters.Count); + var salaryParameter = Assert.IsType(parameters[0]); + Assert.Equal("Salary", salaryParameter.Property.Name); + var overtimeHoursParameter = Assert.IsType(parameters[1]); + Assert.Equal("OvertimeHours", overtimeHoursParameter.Property.Name); + var selectedProps = parsedResult.SelectAndExpand.SelectedItems.OfType().Select(i => i.SelectedPath.LastSegment.Identifier).ToList(); + Assert.Contains("Name", selectedProps); + Assert.Contains("Salary", selectedProps); + Assert.Contains("TaxablePay", selectedProps); + } + + [Fact] + public void ParseUri_WithCustomFunctionInCompute_SingleValuedLiteral_ParsesSelectAndComputeSuccessfully() + { + // Arrange + var odataUriParser = new ODataUriParser( + this.model, + new Uri("http://tempuri.org"), + new Uri($"http://tempuri.org/Employees?$compute=calculateAdjustedSalary(Salary,1%2E5)%20as%20AdjustedSalary&$select=Name,AdjustedSalary")); + + // Act + var parsedResult = odataUriParser.ParseUri(); + + // Assert + Assert.NotNull(parsedResult); + Assert.NotNull(parsedResult.Compute); + var computedItems = parsedResult.Compute.ComputedItems.ToList(); + Assert.Single(computedItems); + var adjustedSalaryItem = computedItems[0]; + Assert.Equal("AdjustedSalary", adjustedSalaryItem.Alias); + var funcCall = Assert.IsType(adjustedSalaryItem.Expression); + Assert.Equal("calculateAdjustedSalary", funcCall.Name); + var parameters = funcCall.Parameters.ToList(); + Assert.Equal(2, parameters.Count); + var salaryParameter = Assert.IsType(parameters[0]); + Assert.Equal("Salary", salaryParameter.Property.Name); + var literalParameter = Assert.IsType(parameters[1]); + Assert.Equal(1.5m, literalParameter.Value); + var selectedProps = parsedResult.SelectAndExpand.SelectedItems.OfType().Select(i => i.SelectedPath.LastSegment.Identifier).ToList(); + Assert.Contains("Name", selectedProps); + Assert.Contains("AdjustedSalary", selectedProps); + } + + [Fact] + public void ParseUri_WithCustomFunctionInCompute_CollectionValuedLiteral_ParsesSelectAndComputeSuccessfully() + { + // Arrange + var odataUriParser = new ODataUriParser( + this.model, + new Uri("http://tempuri.org"), + new Uri($"http://tempuri.org/Employees?$compute=calculateSalaryBand(Salary,[5000,7000,9000])%20as%20SalaryBand&$select=Name,SalaryBand")); + + // Act + var parsedResult = odataUriParser.ParseUri(); + + // Assert + Assert.NotNull(parsedResult); + Assert.NotNull(parsedResult.Compute); + var computedItems = parsedResult.Compute.ComputedItems.ToList(); + Assert.Single(computedItems); + var salaryBandItem = computedItems[0]; + Assert.Equal("SalaryBand", salaryBandItem.Alias); + var funcCall = Assert.IsType(salaryBandItem.Expression); + Assert.Equal("calculateSalaryBand", funcCall.Name); + var parameters = funcCall.Parameters.ToList(); + Assert.Equal(2, parameters.Count); + var salaryParameter = Assert.IsType(parameters[0]); + Assert.Equal("Salary", salaryParameter.Property.Name); + var literalParameter = Assert.IsType(parameters[1]); + var bandValues = literalParameter.Collection.OfType().Select(n => (decimal)n.Value).ToList(); + Assert.Equal(new decimal[] { 5000m, 7000m, 9000m }, bandValues); + var selectedProps = parsedResult.SelectAndExpand.SelectedItems.OfType().Select(i => i.SelectedPath.LastSegment.Identifier).ToList(); + Assert.Contains("Name", selectedProps); + Assert.Contains("SalaryBand", selectedProps); + } + + private void InitializeModel() + { + this.model = new EdmModel(); + var employeeEntityType = this.model.AddEntityType("NS", "Employee"); + var idProperty = employeeEntityType.AddStructuralProperty("ID", EdmCoreModel.Instance.GetInt32(false)); + employeeEntityType.AddKeys(idProperty); + employeeEntityType.AddStructuralProperty("Name", EdmCoreModel.Instance.GetString(false)); + employeeEntityType.AddStructuralProperty("Salary", EdmCoreModel.Instance.GetDecimal(false)); + employeeEntityType.AddStructuralProperty("OvertimeHours", EdmCoreModel.GetCollection(EdmCoreModel.Instance.GetDecimal(false))); + this.model.AddElement(employeeEntityType); + + // Function with single-valued parameter + var calculateBonusEdmFunction = new EdmFunction( + "Default", + "CalculateBonus", + EdmCoreModel.Instance.GetDecimal(false), + true, + null, + false); + calculateBonusEdmFunction.AddParameter("bindingParameter", new EdmEntityTypeReference(employeeEntityType, false)); + calculateBonusEdmFunction.AddParameter("salary", EdmCoreModel.Instance.GetDecimal(false)); + this.model.AddElement(calculateBonusEdmFunction); + + // Function with collection-valued parameter + var calculateOvertimePayEdmFunction = new EdmFunction( + "Default", + "CalculateOvertimePay", + EdmCoreModel.Instance.GetDecimal(false), + true, + null, + false); + calculateOvertimePayEdmFunction.AddParameter("bindingParameter", new EdmEntityTypeReference(employeeEntityType, false)); + calculateOvertimePayEdmFunction.AddParameter("salary", EdmCoreModel.Instance.GetDecimal(false)); + calculateOvertimePayEdmFunction.AddParameter("overtimeHours", EdmCoreModel.GetCollection(EdmCoreModel.Instance.GetDecimal(false))); + this.model.AddElement(calculateOvertimePayEdmFunction); + + // Function with single-valued literal parameter + var getScoreEdmFunction = new EdmFunction( + "Default", + "GetScore", + EdmCoreModel.Instance.GetInt32(false), + true, + null, + false); + getScoreEdmFunction.AddParameter("bindingParameter", new EdmEntityTypeReference(employeeEntityType, false)); + getScoreEdmFunction.AddParameter("maxScore", EdmCoreModel.Instance.GetInt32(false)); + this.model.AddElement(getScoreEdmFunction); + + // Function with collection-valued literal parameter + var getRatingsEdmFunction = new EdmFunction( + "Default", + "GetRating", + EdmCoreModel.Instance.GetInt32(false), + true, + null, + false); + getRatingsEdmFunction.AddParameter("bindingParameter", new EdmEntityTypeReference(employeeEntityType, false)); + getRatingsEdmFunction.AddParameter("scale", EdmCoreModel.GetCollection(EdmCoreModel.Instance.GetInt32(false))); + this.model.AddElement(getRatingsEdmFunction); + + var container = this.model.AddEntityContainer("Default", "Container"); + container.AddEntitySet("Employees", employeeEntityType); + container.AddFunctionImport("CalculateBonus", calculateBonusEdmFunction); + container.AddFunctionImport("CalculateOvertimePay", calculateOvertimePayEdmFunction); + } + } +} diff --git a/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/SemanticAst/CollectionConstantNodeTests.cs b/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/SemanticAst/CollectionConstantNodeTests.cs index c741254b7b..5d205da630 100644 --- a/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/SemanticAst/CollectionConstantNodeTests.cs +++ b/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/SemanticAst/CollectionConstantNodeTests.cs @@ -9,6 +9,7 @@ using Microsoft.OData.Edm; using Xunit; using System.Collections.Generic; +using System.Linq; namespace Microsoft.OData.Tests.UriParser.SemanticAst { @@ -17,11 +18,14 @@ namespace Microsoft.OData.Tests.UriParser.SemanticAst /// public class CollectionConstantNodeTests { + private static IEdmCollectionTypeReference CollectionOf(IEdmTypeReference element) => + new EdmCollectionTypeReference(new EdmCollectionType(element)); + [Fact] public void NumberCollectionThroughLiteralTokenIsSetCorrectly() { const string text = "(1,2,3)"; - var expectedType = new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetInt32(false))); + var expectedType = CollectionOf(EdmCoreModel.Instance.GetInt32(false)); object collection = ODataUriConversionUtils.ConvertFromCollectionValue("[1,2,3]", HardCodedTestModel.TestModel, expectedType); LiteralToken literalToken = new LiteralToken(collection, text, expectedType); @@ -41,7 +45,7 @@ public void NumberCollectionThroughLiteralTokenIsSetCorrectly() public void StringCollectionThroughLiteralTokenIsSetCorrectly() { const string text = "('abc','def','ghi')"; - var expectedType = new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetString(true))); + var expectedType = CollectionOf(EdmCoreModel.Instance.GetString(true)); object collection = ODataUriConversionUtils.ConvertFromCollectionValue("['abc','def','ghi']", HardCodedTestModel.TestModel, expectedType); LiteralToken literalToken = new LiteralToken(collection, text, expectedType); @@ -61,7 +65,7 @@ public void StringCollectionThroughLiteralTokenIsSetCorrectly() public void NullableCollectionTypeSetsConstantNodeCorrectly() { const string text = "('abc','def', null)"; - var expectedType = new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetString(true))); + var expectedType = CollectionOf(EdmCoreModel.Instance.GetString(true)); object collection = ODataUriConversionUtils.ConvertFromCollectionValue("['abc','def', null]", HardCodedTestModel.TestModel, expectedType); LiteralToken literalToken = new LiteralToken(collection, text, expectedType); @@ -81,7 +85,7 @@ public void NullableCollectionTypeSetsConstantNodeCorrectly() public void TextIsSetCorrectly() { const string text = "(1,2,3)"; - var expectedType = new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetInt32(false))); + var expectedType = CollectionOf(EdmCoreModel.Instance.GetInt32(false)); object collection = ODataUriConversionUtils.ConvertFromCollectionValue("[1,2,3]", HardCodedTestModel.TestModel, expectedType); LiteralToken literalToken = new LiteralToken(collection, text, expectedType); @@ -95,7 +99,7 @@ public void TextIsSetCorrectly() public void ItemTypeIsSetCorrectly() { const string text = "(1,2,3)"; - var expectedType = new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetInt32(false))); + var expectedType = CollectionOf(EdmCoreModel.Instance.GetInt32(false)); object collection = ODataUriConversionUtils.ConvertFromCollectionValue("[1,2,3]", HardCodedTestModel.TestModel, expectedType); LiteralToken literalToken = new LiteralToken(collection, text, expectedType); @@ -109,7 +113,7 @@ public void ItemTypeIsSetCorrectly() public void CollectionTypeIsSetCorrectly() { const string text = "(1,2,3)"; - var expectedType = new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetInt32(false))); + var expectedType = CollectionOf(EdmCoreModel.Instance.GetInt32(false)); object collection = ODataUriConversionUtils.ConvertFromCollectionValue("[1,2,3]", HardCodedTestModel.TestModel, expectedType); LiteralToken literalToken = new LiteralToken(collection, text, expectedType); @@ -123,7 +127,7 @@ public void CollectionTypeIsSetCorrectly() public void KindIsSetCorrectly() { const string text = "(1,2,3)"; - var expectedType = new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetInt32(false))); + var expectedType = CollectionOf(EdmCoreModel.Instance.GetInt32(false)); object collection = ODataUriConversionUtils.ConvertFromCollectionValue("[1,2,3]", HardCodedTestModel.TestModel, expectedType); LiteralToken literalToken = new LiteralToken(collection, text, expectedType); @@ -137,11 +141,11 @@ public void KindIsSetCorrectly() public void NullValueShouldThrow() { const string text = "(1,2,3)"; - var expectedType = new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetInt32(false))); + var expectedType = CollectionOf(EdmCoreModel.Instance.GetInt32(false)); object collection = ODataUriConversionUtils.ConvertFromCollectionValue("[1,2,3]", HardCodedTestModel.TestModel, expectedType); LiteralToken literalToken = new LiteralToken(collection, text, expectedType); - Action target = () => new CollectionConstantNode(null, text, expectedType); + Action target = () => new CollectionConstantNode((List)null, text, expectedType); Assert.Throws("objectCollection", target); } @@ -149,7 +153,7 @@ public void NullValueShouldThrow() public void NullLiteralTextShouldThrow() { const string text = "(1,2,3)"; - var expectedType = new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetInt32(false))); + var expectedType = CollectionOf(EdmCoreModel.Instance.GetInt32(false)); object collection = ODataUriConversionUtils.ConvertFromCollectionValue("[1,2,3]", HardCodedTestModel.TestModel, expectedType); LiteralToken literalToken = new LiteralToken(collection, text, expectedType); @@ -161,7 +165,7 @@ public void NullLiteralTextShouldThrow() public void NullCollectionTypeShouldThrow() { const string text = "(1,2,3)"; - var expectedType = new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetInt32(false))); + var expectedType = CollectionOf(EdmCoreModel.Instance.GetInt32(false)); object collection = ODataUriConversionUtils.ConvertFromCollectionValue("[1,2,3]", HardCodedTestModel.TestModel, expectedType); LiteralToken literalToken = new LiteralToken(collection, text, expectedType); @@ -169,6 +173,108 @@ public void NullCollectionTypeShouldThrow() Assert.Throws("collectionType", target); } + [Fact] + public void Constructor_WithConstantNodeCollection_ValidCollectionType_DoesNotThrow() + { + // Arrange + const string literalText = "(1, null, 2)"; + var nodeType = EdmCoreModel.Instance.GetInt32(false); + var collectionType = CollectionOf(nodeType); + var nodes = new List + { + new ConstantNode(1, "1", nodeType), + null, + new ConstantNode(2, "2", nodeType) + }; + + // Act + var result = new CollectionConstantNode(nodes, literalText, collectionType); + + // Assert + Assert.Same(collectionType, result.CollectionType); + Assert.Equal(literalText, result.LiteralText); + Assert.Equal(3, result.Collection.Count); + } + + [Fact] + public void Constructor_WithConstantNodeCollection_NullCollectionType_ThrowsArgumentNull() + { + // Arrange + const string literalText = "(1, null, 2)"; + var nodeType = EdmCoreModel.Instance.GetInt32(false); + var nodes = new List + { + new ConstantNode(1, "1", nodeType), + null, + new ConstantNode(2, "2", nodeType) + }; + + // Act + var ex = Assert.Throws(() => new CollectionConstantNode(nodes, literalText, collectionType: null)); + + // Assert + Assert.Equal("collectionType", ex.ParamName); + Assert.Contains("collectionType", ex.Message, StringComparison.Ordinal); + } + + [Fact] + public void Constructor_WithConstantNodeCollection_NullNodeCollection_ThrowsArgumentNull() + { + // Arrange + const string literalText = "(1,2,3)"; + var nodeType = EdmCoreModel.Instance.GetInt32(false); + var collectionType = CollectionOf(nodeType); + + // Act + var ex = Assert.Throws(() => new CollectionConstantNode((List)null, literalText, collectionType)); + + // Assert + Assert.Equal("nodeCollection", ex.ParamName); + Assert.Contains("nodeCollection", ex.Message, StringComparison.Ordinal); + } + + [Fact] + public void Constructor_WithConstantNodeCollection_NullLiteralText_ThrowsArgumentNull() + { + // Arrange + var nodeType = EdmCoreModel.Instance.GetInt32(false); + var collectionType = CollectionOf(nodeType); + var nodes = new List + { + new ConstantNode(1, "1", nodeType), + new ConstantNode(2, "2", nodeType), + new ConstantNode(3, "3", nodeType) + }; + + // Act + var ex = Assert.Throws(() => new CollectionConstantNode(nodes, literalText: null, collectionType)); + + // Assert + Assert.Equal("literalText", ex.ParamName); + Assert.Contains("literalText", ex.Message, StringComparison.Ordinal); + } + + [Fact] + public void Constructor_WithConstantNodeCollection_EmptyLiteralText_ThrowsArgumentNull() + { + // Arrange + var nodeType = EdmCoreModel.Instance.GetInt32(false); + var collectionType = CollectionOf(nodeType); + var nodes = new List + { + new ConstantNode(1, "1", nodeType), + new ConstantNode(2, "2", nodeType), + new ConstantNode(3, "3", nodeType) + }; + + // Act + var ex = Assert.Throws(() => new CollectionConstantNode(nodes, literalText: string.Empty, collectionType)); + + // Assert + Assert.Equal("literalText", ex.ParamName); + Assert.Contains("literalText", ex.Message, StringComparison.Ordinal); + } + private static void VerifyCollectionConstantNode(IList actual, ConstantNode[] expected) { Assert.NotNull(actual); diff --git a/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/TypePromotionUtilsTests.cs b/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/TypePromotionUtilsTests.cs index 6087e676bd..5910756a3e 100644 --- a/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/TypePromotionUtilsTests.cs +++ b/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/TypePromotionUtilsTests.cs @@ -6,8 +6,9 @@ using System; using System.Linq; -using Microsoft.OData.UriParser; +using System.Reflection; using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; using Xunit; namespace Microsoft.OData.Tests.UriParser @@ -18,6 +19,23 @@ namespace Microsoft.OData.Tests.UriParser public class TypePromotionUtilsTests { #region PromoteOperandTypes Tests (For Binary Operators) + private static MethodInfo SingleValueCanPromote => + typeof(TypePromotionUtils).GetMethods(BindingFlags.NonPublic | BindingFlags.Static) + .First(m => m.Name == "CanPromoteNodeTo" && m.GetParameters()[0].ParameterType == typeof(SingleValueNode)); + + private static MethodInfo CollectionCanPromote => + typeof(TypePromotionUtils).GetMethods(BindingFlags.NonPublic | BindingFlags.Static) + .First(m => m.Name == "CanPromoteNodeTo" && m.GetParameters()[0].ParameterType == typeof(CollectionNode)); + + private static bool InvokeSingle(SingleValueNode node, IEdmTypeReference sourceType, IEdmTypeReference targetType) => + (bool)SingleValueCanPromote.Invoke(null, new object[] { node, sourceType, targetType }); + + private static bool InvokeCollection(CollectionNode node, IEdmTypeReference sourceType, IEdmTypeReference targetType) => + (bool)CollectionCanPromote.Invoke(null, new object[] { node, sourceType, targetType }); + + private static IEdmCollectionTypeReference CollectionOf(IEdmTypeReference element) => + new EdmCollectionTypeReference(new EdmCollectionType(element)); + [Fact] public void EqualsOnComplexAndNullIsSupported() { @@ -609,7 +627,7 @@ internal void DefaultTypeFacetsPromotionRulesTestFilter(bool nullable) Assert.Equal(6, ((IEdmDecimalTypeReference)((SingleValuePropertyAccessNode)unaryNode.Operand).TypeReference).Precision); Assert.Equal(3, ((IEdmDecimalTypeReference)((SingleValuePropertyAccessNode)unaryNode.Operand).TypeReference).Scale); - // GetAdditionTermporalSignatures: date, duration + // GetAdditionTemporalSignatures: date, duration var dateTypeDefinition = EdmCoreModel.Instance.GetDate(nullable).Definition; tree = new ODataUriParser(model, svcRoot, new Uri("http://host/Set?$filter=Date add Duration_6 ne 2016-08-18", UriKind.Absolute)).ParseUri().Filter; binaryNode = (BinaryOperatorNode)((BinaryOperatorNode)tree.Expression).Left; @@ -682,7 +700,7 @@ internal void DefaultTypeFacetsPromotionRulesTestOrderBy(bool nullable) Assert.Equal(6, ((IEdmDecimalTypeReference)((SingleValuePropertyAccessNode)unaryNode.Operand).TypeReference).Precision); Assert.Equal(3, ((IEdmDecimalTypeReference)((SingleValuePropertyAccessNode)unaryNode.Operand).TypeReference).Scale); - // GetAdditionTermporalSignatures: date, duration + // GetAdditionTemporalSignatures: date, duration var dateTypeDefinition = EdmCoreModel.Instance.GetDate(nullable).Definition; tree = new ODataUriParser(model, svcRoot, new Uri("http://host/Set?$orderby=Date add Duration_6", UriKind.Absolute)).ParseUri().OrderBy; binaryNode = (BinaryOperatorNode)tree.Expression; @@ -700,6 +718,239 @@ public void CustomTypeFacetsPromotionRulesTest() CustomTypeFacetsPromotionRulesTestOrderBy(true); } + // SingleValueNode overload + + [Fact] + public void Single_NullSourceType_NullableTargetPrimitive_Promotes() + { + var target = EdmCoreModel.Instance.GetInt32(true); + Assert.True(InvokeSingle(null, null, target)); + } + + [Fact] + public void Single_NullSourceType_NonNullableTargetPrimitive_DoesNotPromote() + { + var target = EdmCoreModel.Instance.GetInt32(false); + Assert.False(InvokeSingle(null, null, target)); + } + + [Fact] + public void Single_ExactMatch_Promotes() + { + var t = EdmCoreModel.Instance.GetString(false); + var node = new ConstantNode("abc", "'abc'", t); + Assert.True(InvokeSingle(node, t, t)); + } + + [Fact] + public void Single_NullableToNonNullableSameValueType_Promotes() + { + var src = EdmCoreModel.Instance.GetInt32(true); + var dst = EdmCoreModel.Instance.GetInt32(false); + var node = new ConstantNode(5, "5", src); + Assert.True(InvokeSingle(node, src, dst)); + } + + [Fact] + public void Single_IncompatiblePrimitiveTypes_Fails() + { + var src = EdmCoreModel.Instance.GetString(true); + var dst = EdmCoreModel.Instance.GetInt32(false); + var node = new ConstantNode("abc", "'abc'", src); + Assert.False(InvokeSingle(node, src, dst)); + } + + [Fact] + public void Single_NullableStringToNonNullableInt_Fails() + { + var src = EdmCoreModel.Instance.GetString(true); + var dst = EdmCoreModel.Instance.GetInt32(false); + var node = new ConstantNode("123", "'123'", src); + Assert.False(InvokeSingle(node, src, dst)); + } + + // CollectionNode overload + + [Fact] + public void Collection_NullSourceType_NullableTargetCollection_Promotes() + { + var target = CollectionOf(EdmCoreModel.Instance.GetInt32(true)); + Assert.True(InvokeCollection(null, null, target)); + } + + [Fact] + public void Collection_NullSourceType_NonNullableTargetCollection_Fails() + { + var target = CollectionOf(EdmCoreModel.Instance.GetInt32(false)); + Assert.False(InvokeCollection(null, null, target)); + } + + [Fact] + public void Collection_EquivalentElementTypes_Promotes() + { + var elem = EdmCoreModel.Instance.GetString(true); + var srcType = CollectionOf(elem); + var dstType = CollectionOf(elem); + var node = new CollectionConstantNode(new object[] { "a", "b" }, "['a','b']", srcType); + Assert.True(InvokeCollection(node, srcType, dstType)); + } + + [Fact] + public void Collection_NullableElementToNonNullableElement_Promotes() + { + var srcElem = EdmCoreModel.Instance.GetInt32(true); + var dstElem = EdmCoreModel.Instance.GetInt32(false); + var srcType = CollectionOf(srcElem); + var dstType = CollectionOf(dstElem); + var node = new CollectionConstantNode(new object[] { 1, 2 }, "[1,2]", srcType); + Assert.True(InvokeCollection(node, srcType, dstType)); + } + + [Fact] + public void Collection_IncompatibleElementTypes_Fails() + { + var srcType = CollectionOf(EdmCoreModel.Instance.GetString(true)); + var dstType = CollectionOf(EdmCoreModel.Instance.GetInt32(false)); + var node = new CollectionConstantNode(new object[] { "1", "2" }, "['1','2']", srcType); + Assert.False(InvokeCollection(node, srcType, dstType)); + } + + [Fact] + public void Collection_NullableStringElementsToNonNullableIntElements_Fails() + { + var srcType = CollectionOf(EdmCoreModel.Instance.GetString(true)); + var dstType = CollectionOf(EdmCoreModel.Instance.GetInt32(false)); + var node = new CollectionConstantNode(new object[] { "10", "11" }, "['10','11']", srcType); + Assert.False(InvokeCollection(node, srcType, dstType)); + } + + // QueryNode overload + + [Fact] + public void Query_NullSourceType_NullablePrimitiveTarget_Promotes() + { + var target = EdmCoreModel.Instance.GetBoolean(true); + Assert.True(TypePromotionUtils.CanPromoteNodeTo(null, null, target)); + } + + [Fact] + public void Query_NullSourceType_NonNullablePrimitiveTarget_Fails() + { + var target = EdmCoreModel.Instance.GetBoolean(false); + Assert.False(TypePromotionUtils.CanPromoteNodeTo(null, null, target)); + } + + [Fact] + public void Query_CollectionNodeCannotPromoteToSingleValue() + { + var elem = EdmCoreModel.Instance.GetInt32(false); + var collType = CollectionOf(elem); + var collNode = new CollectionConstantNode(new object[] { 1 }, "[1]", collType); + var targetSingle = EdmCoreModel.Instance.GetInt32(false); + Assert.False(TypePromotionUtils.CanPromoteNodeTo(collNode, collType, targetSingle)); + } + + [Fact] + public void Query_BracketedIntLiteral_ToIntCollection_Promotes() + { + var constant = new ConstantNode("[1,2,3]", "[1,2,3]", EdmCoreModel.Instance.GetString(true)); + var targetElem = EdmCoreModel.Instance.GetInt32(false); + var targetColl = CollectionOf(targetElem); + Assert.True(TypePromotionUtils.CanPromoteNodeTo(constant, constant.TypeReference, targetColl)); + } + + [Fact] + public void Query_BracketedStringLiteral_ToStringCollection_Promotes() + { + var constant = new ConstantNode("['a','b']", "['a','b']", EdmCoreModel.Instance.GetString(true)); + var targetElem = EdmCoreModel.Instance.GetString(false); + var targetColl = CollectionOf(targetElem); + Assert.True(TypePromotionUtils.CanPromoteNodeTo(constant, constant.TypeReference, targetColl)); + } + + [Fact] + public void Query_BracketedBoolLiteral_ToIntCollection_Fails() + { + var constant = new ConstantNode("[true,false]", "[true,false]", EdmCoreModel.Instance.GetString(true)); + var targetElem = EdmCoreModel.Instance.GetInt32(false); + var targetColl = CollectionOf(targetElem); + Assert.False(TypePromotionUtils.CanPromoteNodeTo(constant, constant.TypeReference, targetColl)); + } + + [Fact] + public void Query_EmptyCollectionLiteral_ToNonNullableElementCollection_Promotes() + { + var constant = new ConstantNode("[]", "[]", EdmCoreModel.Instance.GetString(true)); + var targetElem = EdmCoreModel.Instance.GetString(false); + var targetColl = CollectionOf(targetElem); + Assert.True(TypePromotionUtils.CanPromoteNodeTo(constant, constant.TypeReference, targetColl)); + } + + [Fact] + public void Query_AllNullItemsLiteral_ToNullableElementCollection_Promotes() + { + var constant = new ConstantNode("[null,null]", "[null,null]", EdmCoreModel.Instance.GetString(true)); + var targetElem = EdmCoreModel.Instance.GetInt32(true); + var targetColl = CollectionOf(targetElem); + Assert.True(TypePromotionUtils.CanPromoteNodeTo(constant, constant.TypeReference, targetColl)); + } + + [Fact] + public void Query_AllNullItemsLiteral_ToNonNullableElementCollection_Fails() + { + var constant = new ConstantNode("[null,null]", "[null,null]", EdmCoreModel.Instance.GetString(true)); + var targetElem = EdmCoreModel.Instance.GetInt32(false); + var targetColl = CollectionOf(targetElem); + Assert.False(TypePromotionUtils.CanPromoteNodeTo(constant, constant.TypeReference, targetColl)); + } + + [Fact] + public void Query_BracketedIntLiteral_ToStringCollection_Fails() + { + var constant = new ConstantNode("[1,2,3]", "[1,2,3]", EdmCoreModel.Instance.GetString(true)); + var targetElem = EdmCoreModel.Instance.GetString(false); + var targetColl = CollectionOf(targetElem); + Assert.False(TypePromotionUtils.CanPromoteNodeTo(constant, constant.TypeReference, targetColl)); + } + + [Fact] + public void Query_NonBracketedStringLiteral_CannotPromoteToCollection() + { + var constant = new ConstantNode("abc", "'abc'", EdmCoreModel.Instance.GetString(true)); + var targetElem = EdmCoreModel.Instance.GetString(false); + var targetColl = CollectionOf(targetElem); + Assert.False(TypePromotionUtils.CanPromoteNodeTo(constant, constant.TypeReference, targetColl)); + } + + [Fact] + public void Query_ExistingCollectionNode_NullableToNonNullableElement_Promotes() + { + var srcElem = EdmCoreModel.Instance.GetInt32(true); + var dstElem = EdmCoreModel.Instance.GetInt32(false); + var srcType = CollectionOf(srcElem); + var dstType = CollectionOf(dstElem); + var node = new CollectionConstantNode(new object[] { 10, 11 }, "[10,11]", srcType); + Assert.True(TypePromotionUtils.CanPromoteNodeTo(node, srcType, dstType)); + } + + [Fact] + public void Query_ExistingCollectionNode_IncompatibleElementTypes_Fails() + { + var srcType = CollectionOf(EdmCoreModel.Instance.GetString(true)); + var dstType = CollectionOf(EdmCoreModel.Instance.GetInt32(false)); + var node = new CollectionConstantNode(new object[] { "x", "y" }, "['x','y']", srcType); + Assert.False(TypePromotionUtils.CanPromoteNodeTo(node, srcType, dstType)); + } + + [Fact] + public void Query_BracketedWhitespaceLiteral_IntCollection_Promotes() + { + var constant = new ConstantNode("[ 1 , 2 , 3 ]", "[ 1 , 2 , 3 ]", EdmCoreModel.Instance.GetString(true)); + var targetElem = EdmCoreModel.Instance.GetInt32(false); + var targetColl = CollectionOf(targetElem); + Assert.True(TypePromotionUtils.CanPromoteNodeTo(constant, constant.TypeReference, targetColl)); + } + internal class CustomTypeFacetsPromotionRules : TypeFacetsPromotionRules { public override int? GetPromotedPrecision(int? left, int? right) @@ -790,7 +1041,7 @@ internal void CustomTypeFacetsPromotionRulesTestFilter(bool nullable) Assert.Equal(6, ((IEdmDecimalTypeReference)((SingleValuePropertyAccessNode)unaryNode.Operand).TypeReference).Precision); Assert.Equal(3, ((IEdmDecimalTypeReference)((SingleValuePropertyAccessNode)unaryNode.Operand).TypeReference).Scale); - // GetAdditionTermporalSignatures: date, duration + // GetAdditionTemporalSignatures: date, duration var dateTypeDefinition = EdmCoreModel.Instance.GetDate(nullable).Definition; parser = new ODataUriParser(model, svcRoot, new Uri("http://host/Set?$filter=Date add Duration_6 ne 2016-08-18", UriKind.Absolute)); parser.Resolver.TypeFacetsPromotionRules = new CustomTypeFacetsPromotionRules(); @@ -874,7 +1125,7 @@ internal void CustomTypeFacetsPromotionRulesTestOrderBy(bool nullable) Assert.Equal(6, ((IEdmDecimalTypeReference)((SingleValuePropertyAccessNode)unaryNode.Operand).TypeReference).Precision); Assert.Equal(3, ((IEdmDecimalTypeReference)((SingleValuePropertyAccessNode)unaryNode.Operand).TypeReference).Scale); - // GetAdditionTermporalSignatures: date, duration + // GetAdditionTemporalSignatures: date, duration var dateTypeDefinition = EdmCoreModel.Instance.GetDate(nullable).Definition; parser = new ODataUriParser(model, svcRoot, new Uri("http://host/Set?$orderby=Date add Duration_6", UriKind.Absolute)); parser.Resolver.TypeFacetsPromotionRules = new CustomTypeFacetsPromotionRules();