Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Fixes #1147: Enable skiptoken handler to handler advanced orderby clause #1164

Merged
merged 4 commits into from
Feb 29, 2024

Conversation

xuzhg
Copy link
Member

@xuzhg xuzhg commented Jan 30, 2024

Fixes #1147

Two parts:

1. How to generate the skip token when we have the advanced $orderby?

Based on the discussion and back-compatibility, the PR follows up:
a) If it's top level property orderby (declared or dynamic or computed), for example: $orderby=name
    the skiptoken will follow up the existing pattern:   `name-'abc'` 

b) If it's other orderby, for example: $orderby=tolower(substring(name,1,2))
    the skiptoken will only contains the value:   `tx`

2. How to consume the skip token when apply the $skiptoken?

Basically, the skiptoken is coverted to a 'Where(...)' clause (Linq Where function call). Behind the scene, it's a $filter clause.
The existing implementation cannot handle all and it contains errors.
This PR is to change it and based on the above truth, we should use our IFilterBinder to handle the $skiptoken.

The logic is that:
a) Convert the skiptoken and orderby to filter expression
b) Call IFilterBinder to bind the filter expression to 'Where' Linq expression
c) Apply the 'Where' to IQueryable. 

{
if (lastMember == null)
{
return string.Empty;
}

IEnumerable<IEdmProperty> propertiesForSkipToken = GetPropertiesForSkipToken(lastMember, model, orderByNodes);
StringBuilder skipTokenBuilder = new StringBuilder(String.Empty);
Copy link
Member Author

@xuzhg xuzhg Jan 30, 2024

Choose a reason for hiding this comment

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

Here's the part 1 refactor. #WontFix

{
Contract.Assert(query != null);
Copy link
Member Author

@xuzhg xuzhg Jan 30, 2024

Choose a reason for hiding this comment

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

Here's the part 2 refactor #WontFix

Copy link
Contributor

@habbes habbes left a comment

Choose a reason for hiding this comment

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

Partial review. Will share more feedback later.

/// </summary>
/// <param name="clause">The input orderby clause, the 'ThenBy' in each node does matter.</param>
/// <returns>The output orderby clauses, the 'ThenBy' in each node does NOT matter.</returns>
public static IList<OrderByClause> ToList(this OrderByClause clause)
Copy link
Contributor

@habbes habbes Feb 8, 2024

Choose a reason for hiding this comment

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

Since this class is internal, I would suggest returning a List<T> instead of IList<T> since using List<T> directly is more efficient. For example when doing a foreach against an IList<T> it will box and allocate the enumerator to the heap, but when using List<T> directly you avoid boxing the enumerator. #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

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

Keep eyes on List is more efficent than IList. Since it's internal, I am ok to replace it using List.

}

// Avoid duplicated binding.
ISet<string> usedPropertyNames = new HashSet<string>();
Copy link
Contributor

@habbes habbes Feb 8, 2024

Choose a reason for hiding this comment

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

For internal, private or local variables, I suggest we use concrete types. Using interfaces lead to more boxing of enumerators and methods are less likely to get inlined by the JIT.

We should consider interfaces for public APIs or for scenarios where we actually want to support different implementations. But in this case, we have no reason to support other implementations than HashSet<string>.
#Resolved

Copy link
Member Author

Choose a reason for hiding this comment

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

Maybe you can give us a 'session' about this.


Expression propertyName;
Expression propertyValue;
IList<string> names = new List<string>();
Copy link
Contributor

@habbes habbes Feb 8, 2024

Choose a reason for hiding this comment

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

Suggested change
IList<string> names = new List<string>();
List<string> names = new List<string>();
``` #Resolved

Copy link
Contributor

Choose a reason for hiding this comment

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

Is it possible to estimate the capacity of the list from the input so that we avoid resizing?

}
}

private static string GetOrderByName(ISet<string> usedPropertyNames, ref int start)
Copy link
Contributor

@habbes habbes Feb 8, 2024

Choose a reason for hiding this comment

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

Suggested change
private static string GetOrderByName(ISet<string> usedPropertyNames, ref int start)
private static string GetOrderByName(HashSet<string> usedPropertyNames, ref int start)
``` #Resolved


name = GetOrderByName(usedPropertyNames, ref start);
names.Add(name);
usedPropertyNames.Add(name);
Copy link
Contributor

@habbes habbes Feb 8, 2024

Choose a reason for hiding this comment

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

Given that each name is generated by adding an incrementing counter, each iteration will produce a unique name. In that case, do we still need to add to these auto-generated names to the usedPropertyNames set? #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

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

Good Suggestion. Let me comment it out and leave a comment for it.

Expression propertyValue;
IList<string> names = new List<string>();
string name;
int start = 1;
Copy link
Contributor

@habbes habbes Feb 8, 2024

Choose a reason for hiding this comment

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

Suggested change
int start = 1;
int index = 1;
``` #Resolved

@@ -679,6 +680,9 @@ private static IEnumerable<IEdmStructuralProperty> GetAvailableOrderByProperties
: entityType
.StructuralProperties()
.Where(property => property.Type.IsPrimitive() && !property.Type.IsStream())

// @discuss: Orderby the keys using the key name alphabet order doesn't make sense for me.
// Since we don't have the 'Order' value, we should keep the order same as definition order in the schema?
Copy link
Contributor

@habbes habbes Feb 8, 2024

Choose a reason for hiding this comment

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

Maybe the goal was to ensure the properties are always returned in a consistent order. Maybe any order is valid so long as it's consistent between calls. But I don't know the context.
But if there's something we can do to void sorting, I'd vouch for that. We saw some a lot of cycles from sorting and other LINQ operations related to ETag properties, and I think we should avoid that where possible. #Pending

Copy link
Member Author

Choose a reason for hiding this comment

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

I'd like to here input from @mikepizzo also.
It's just for discussion. I will leave this code unchanged since change it is a behavior breaking changes.

IEnumerable<IEdmProperty> propertiesForSkipToken = GetPropertiesForSkipToken(lastMember, model, orderByNodes);
StringBuilder skipTokenBuilder = new StringBuilder(String.Empty);
if (propertiesForSkipToken == null)
IList<OrderByClause> clauses = GetOrderByClauses(lastMember, model, clause);
Copy link
Contributor

@habbes habbes Feb 9, 2024

Choose a reason for hiding this comment

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

Suggested change
IList<OrderByClause> clauses = GetOrderByClauses(lastMember, model, clause);
List<OrderByClause> clauses = GetOrderByClauses(lastMember, model, clause);
``` #Resolved

}

return skipTokenBuilder.ToString();
}

private static IList<KeyValuePair<string, object>> GetPropertyValues(object source, IEdmModel model, IList<OrderByClause> clauses,
Copy link
Contributor

@habbes habbes Feb 9, 2024

Choose a reason for hiding this comment

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

Suggested change
private static IList<KeyValuePair<string, object>> GetPropertyValues(object source, IEdmModel model, IList<OrderByClause> clauses,
private static List<KeyValuePair<string, object>> GetPropertyValues(object source, IEdmModel model, List<OrderByClause> clauses,
``` #Resolved

IList<OrderByClause> clauses = GetOrderByClauses(lastMember, model, clause);

TimeZoneInfo timeZoneInfo = context?.TimeZone;
IList<KeyValuePair<string, object>> values = GetPropertyValues(lastMember, model, clauses, context);
Copy link
Contributor

@habbes habbes Feb 9, 2024

Choose a reason for hiding this comment

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

Suggested change
IList<KeyValuePair<string, object>> values = GetPropertyValues(lastMember, model, clauses, context);
IList<KeyValuePair<string, object>> values = GetPropertyValues(lastMember, model, clauses, context);
Suggested change
IList<KeyValuePair<string, object>> values = GetPropertyValues(lastMember, model, clauses, context);
List<KeyValuePair<string, object>> values = GetPropertyValues(lastMember, model, clauses, context);
``` #Resolved


structuredObj.TryGetPropertyValue(OrderByClauseHelpers.OrderByGlobalNameKey, out object orderByNameObject);

IList<KeyValuePair<string, object>> values = new List<KeyValuePair<string, object>>();
Copy link
Contributor

@habbes habbes Feb 9, 2024

Choose a reason for hiding this comment

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

Suggested change
IList<KeyValuePair<string, object>> values = new List<KeyValuePair<string, object>>();
List<KeyValuePair<string, object>> values = new List<KeyValuePair<string, object>>();
``` #Resolved

}

return skipTokenBuilder.ToString();
}

private static IList<KeyValuePair<string, object>> GetPropertyValues(object source, IEdmModel model, IList<OrderByClause> clauses,
Copy link
Contributor

@habbes habbes Feb 9, 2024

Choose a reason for hiding this comment

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

Is this method called when consuming skip tokens? #WontFix

Copy link
Member Author

Choose a reason for hiding this comment

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

No, It's for 'Part-1' to generate the Skip Token string.

@@ -464,28 +456,35 @@ internal static IEnumerable<IEdmProperty> GetPropertiesForSkipToken(object lastM
return null;
}

IEnumerable<IEdmProperty> key = entity.Key();
if (orderByNodes != null)
IList<OrderByClause> orderByClauses = new List<OrderByClause>();
Copy link
Contributor

@habbes habbes Feb 12, 2024

Choose a reason for hiding this comment

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

Suggested change
IList<OrderByClause> orderByClauses = new List<OrderByClause>();
List<OrderByClause> orderByClauses = new List<OrderByClause>();
``` #Resolved

IEnumerable<IEdmProperty> key = entity.Key();
if (orderByNodes != null)
IList<OrderByClause> orderByClauses = new List<OrderByClause>();
ISet<IEdmProperty> properties = new HashSet<IEdmProperty>();
Copy link
Contributor

@habbes habbes Feb 12, 2024

Choose a reason for hiding this comment

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

Suggested change
ISet<IEdmProperty> properties = new HashSet<IEdmProperty>();
HashSet<IEdmProperty> properties = new HashSet<IEdmProperty>();
``` #Resolved

#region Data
static SkipTokenControllers()
{
IList<StAddress> stAddresses = new List<StAddress>()
Copy link
Contributor

@habbes habbes Feb 12, 2024

Choose a reason for hiding this comment

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

Do you have test cases with dates and negative numbers? #Resolved

// Since we don't have such methods, let's simply split the request raw value.
IList<string> orderBys = orderByOption.RawValue.Split(',');
IList<OrderByClause> orderByClauses = orderByOption.OrderByClause.ToList();
IList<string> tokenValueParis = PopulatePropertyValuePairs(skipTokenRawValue);
Copy link
Contributor

@habbes habbes Feb 22, 2024

Choose a reason for hiding this comment

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

Suggested change
IList<string> tokenValueParis = PopulatePropertyValuePairs(skipTokenRawValue);
IList<string> tokenValuePairs = PopulatePropertyValuePairs(skipTokenRawValue);
``` #Resolved

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually, since this list only contains the values (without keys), why not just tokenValues?

Suggested change
IList<string> tokenValueParis = PopulatePropertyValuePairs(skipTokenRawValue);
List<string> tokenValues = PopulatePropertyValuePairs(skipTokenRawValue);


foreach (KeyValuePair<string, (object PropertyValue, Type PropertyType)> item in propertyValuePairs)
string where = string.Empty;
string lastEquality = null;
Copy link
Contributor

@habbes habbes Feb 23, 2024

Choose a reason for hiding this comment

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

nit: The name lastEquality might give the impression that this refers to the last or final equality in the loop, but actually it refers to the equality generated in the previous iteration.

Suggested change
string lastEquality = null;
string previousEquality = null;
``` #Resolved

IDictionary<string, (object PropertyValue, Type PropertyType)> propertyValuePairs = PopulatePropertyValuePairs(skipTokenRawValue, context);
// It's better to visit the nodes of OrderByClause reclusively to get the orderby raw value.
// Since we don't have such methods, let's simply split the request raw value.
IList<string> orderBys = orderByOption.RawValue.Split(',');
Copy link
Contributor

@habbes habbes Feb 23, 2024

Choose a reason for hiding this comment

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

This will fail if for non-trivial orderby expressions. For example, if you have a function call with multiple parameters like $orderby=substring(Field, 1),Id, this will splitinto: ["substring(Field", "1)", "Id"] which is invalid and will lead to an argument out of range exception because orderBys list will have more items than orderByClauses list.

Did you intend to support these scenarios in this PR or in a future one? If you want to add support for that in the future then we should detect such cases and throw a meaningful exception that lets the user know that operation is not supported, and then we should create an issue to support that. Otherwise we should handle it better in this PR. Do we have an method that can generate a string representation of an orderByClause.Expression that is a valid OData query expression?

image
#Resolved

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed. Can you help to test it again?

1) Can handle the 'asc' suffix
2) Can handle the ',' within expression
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.

@odata.nextLink doesn't work when query contains $orderby=parentEntity/childEntity
2 participants