Skip to content

Conversation

@WanjohiSammy
Copy link
Member

@WanjohiSammy WanjohiSammy commented Nov 12, 2024

Issues

This pull request fixes #1744.

Description

According to OData V4 spec isof and cast function type parameter can be used without enclosed in quotes.

The isof function has the following signatures

Edm.Boolean isof(type)
Edm.Boolean isof(expression,type)

This update supports unquoted type parameters in isof and cast functions through the following changes:

  • Add support to handle SingleResourceCastNode in ValidateIsOfOrCast method since the unquoted type parameter is bind as SingleResourceCastNode
  • Update the FunctionCallParser to handle isof and cast function calls with two parameters, where the second parameter is an unquoted type parameter. For two parameters, the second unquoted type parameter's next token should point to the first parameter. Previously, this was only handled for quoted type parameters. The change has now been made to include unquoted type parameters as well.
    For example,
    • Cast(Location, NS.HomeAddress) -> NS.HomeAddress will be a DottedIdentifierToken, and its NextToken should point to the Location parameter token.
    • Isof(Location, NS.HomeAddress) -> This also applies to the isof() function.

With this change, the following queries with unquoted type param are now supported:

cast(NS.Employee) // cast Person to Employee
cast(Location, NS.WorkAddress) // cast Location property to NS.WorkAddress
isof(NS.Employee) // cast Person to Employee
isof(Location, NS.WorkAddress) // cast Location property to NS.WorkAddress

Lets say you have the following data model:

namespace NS;

public class Product
{
    public int ProductID { get; set; }
    // Others properties
    public Address SupplierAddress { get; set; }
}

public class DerivedProduct : Product
{
   public string AnotherProperty { get; set; }
    // Others properties
}

public class Address
{
    // Others properties
    public string City { get; set; }
}

Executing the query isof(NS.DerivedCategory) will throw the exception:

"Encountered invalid type cast. 'NS.Category' is not assignable from 'NS.Product'."

This occurs because ODL checks if NS.Product is related to NS.Category using the CheckRelatedTo function and since the types are not related or one is not a derivative of the other, the exception is thrown.

Main changes

This change aims to fix issue #3123 by binding the EdmPrimitiveType DottedIdentifierToken to ConstantNode instead of SingleValueCastNode. This is by ensuring that the nextToken of the EdmPrimitiveType DottedIdentifierToken (e.g., Edm.Int64, Edm.String, Edm.Single, etc.) is null if the DottedIdentifierToken is a primitive type even if cast or isof has 2 arguments e.g, cast(ID, Edm.Int32) lt 10, isof(MyAddress, Fully.Qualified.Namespace.HomeAddress), etc

Checklist (Uncheck if it is not completed)

  • Test cases added
  • Build and test with one-click build and test script passed

Additional work necessary

If documentation update is needed, please add "Docs Needed" label to the issue and provide details about the required document change in the issue.

xuzhg
xuzhg previously approved these changes Nov 12, 2024
xuzhg
xuzhg previously approved these changes Nov 18, 2024
xuzhg
xuzhg previously approved these changes Nov 20, 2024
@WanjohiSammy WanjohiSammy changed the title Fix type casting exceptions in 'isof' and 'cast' calls. Fix 'isof' and 'cast' unquoted type params issue Nov 21, 2024
@WanjohiSammy WanjohiSammy force-pushed the fix/impossible-cast-with-isof-exception branch from bdf86ef to f2c4d60 Compare November 28, 2024 09:39
@WanjohiSammy WanjohiSammy requested review from habbes and xuzhg November 28, 2024 10:36
@WanjohiSammy WanjohiSammy requested a review from Copilot December 6, 2024 06:31
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot reviewed 4 out of 4 changed files in this pull request and generated no suggestions.

@WanjohiSammy
Copy link
Member Author

/AzurePipelines run

@azure-pipelines
Copy link

No pipelines are associated with this pull request.

@habbes
Copy link
Contributor

habbes commented Aug 7, 2025

@WanjohiSammy the issues listed in the description that this PR aims to fix happen to be closed. What is the exact behaviour that this PR aims to change or rectify? Is it to support case-insensitivity in the isof and cast operator or supporting isof on unrelated types without throwing an error? It's not clear to me from reading the description what did not work before vs what now works after this PR.

@WanjohiSammy WanjohiSammy requested a review from habbes August 7, 2025 13:38
@WanjohiSammy WanjohiSammy requested a review from habbes August 7, 2025 15:55
@habbes
Copy link
Contributor

habbes commented Aug 8, 2025

Both filters would yield the same result—an empty set—because there are no DerivedProduct instances in the collection, and Category is unrelated to Product.
While this behavior is technically correct per the spec, it raises a usability concern: if a user mistakenly writes isof(NS.Category) thinking it's a valid filter, the system silently returns no results. This could lead to confusion or debugging difficulties, especially when the type is completely unrelated.
I understand the idea of staying spec-compliant but how can we help users catch likely mistakes when the cast target is unrelated to the base type?
Curious to hear your thoughts on this trade-off. cc @mikepizzo

@gathogojr I have updated the PR description of what the change is about. I have also added tests to ensure that exceptions are thrown when the types do not match.

In my view, it makes more sense to return an empty collection or false if the types are not related rather than to throw an error. The same way if (string is List<int>) returns false (though it returns a warning) and IEnumerable<int>().OfType<string>() would return an empty collection. But if this would be a regression, I'm okay with preserving the existing behaviour (if it's throwing an exception). But this is not a hill I'm willing to die on. Both approaches have merit. cc @gathogojr

/// Provides efficient push, pop, and peek operations with dynamic resizing.
/// </summary>
/// <typeparam name="T">The type of elements in the stack.</typeparam>
internal struct StackStruct<T>
Copy link
Contributor

@habbes habbes Aug 8, 2025

Choose a reason for hiding this comment

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

This is only light-weight in the wrapper StackStruct is a struct and not an object. So it may save one allocation, but you still use a heap allocated array even for the base case. So It's not really much different from a regular Stack and I don't think it's worth creating a custom type.

This is different from the version I proposed which would use an inline array buffer (e.g. using InlineArray attribute, could use fixed array buffer, but the latter is unsafe and limited to simple primitive types).

But I wouldn't want to block this PR because of this. My suggestion would be to remove this altogether (since it doesn't provide much optimization over a standard Stack, then we can think of a lightweight stack helper in a separate PR that can be used across the codebase)

Copy link
Member Author

Choose a reason for hiding this comment

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

@habbes I have reverted to use the regular stack

@WanjohiSammy WanjohiSammy requested review from Copilot and habbes August 8, 2025 07:23
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This pull request fixes the handling of unquoted type parameters in OData 'isof' and 'cast' functions to align with the OData V4 specification, which allows type parameters to be used without quotes.

Key changes include:

  • Adding support for SingleResourceCastNode handling in the ValidateIsOfOrCast method
  • Updating the FunctionCallParser to properly link unquoted type parameters with their corresponding expression parameters through the NextToken property
  • Ensuring primitive type parameters (like Edm.Int32) are bound as ConstantNode instead of SingleValueCastNode when unquoted

Reviewed Changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

File Description
FunctionCallParser.cs Enhanced argument parsing logic to handle unquoted type parameters in cast/isof functions by setting proper parent-child relationships
FunctionCallBinder.cs Added support for SingleResourceCastNode in type validation and improved case-insensitive string comparison
NormalizedModelElementsCache.cs Added shared cache instance for EDM core model to support primitive type detection
Test files Comprehensive test coverage for both quoted and unquoted type parameters with various case sensitivity scenarios

Comment on lines 661 to +666
return new ODataQueryOptionParser(edmModel, entityType, edmEntitySet, new Dictionary<string, string>() { { "$filter", text } }).ParseFilter();
}

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

Copilot AI Aug 8, 2025

Choose a reason for hiding this comment

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

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

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

Copilot uses AI. Check for mistakes.
@WanjohiSammy WanjohiSammy merged commit 1fa435d into main Aug 11, 2025
2 checks passed
@WanjohiSammy WanjohiSammy deleted the fix/impossible-cast-with-isof-exception branch August 11, 2025 15:23
WanjohiSammy added a commit that referenced this pull request Aug 11, 2025
* Add support for unquoted type params in `isof` and `cast` methods

---------

Co-authored-by: Clément Habinshuti <[email protected]>
WanjohiSammy added a commit that referenced this pull request Aug 19, 2025
* Add support for unquoted type params in `isof` and `cast` methods

---------

Co-authored-by: Clément Habinshuti <[email protected]>
WanjohiSammy added a commit that referenced this pull request Aug 19, 2025
* Add support for unquoted type params in `isof` and `cast` methods

---------

Co-authored-by: Clément Habinshuti <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Isof type parameter quotes

7 participants