-
Notifications
You must be signed in to change notification settings - Fork 933
NH-3850 - Polymorphic Linq query results aggregators failures #584
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
NH-3850 - Polymorphic Linq query results aggregators failures #584
Conversation
08abfa6
to
bb5a9fb
Compare
@gliljas, @hazzik, does the change on
|
I'm not sure. It satisfies the LINQ contract (which is probably paramount) but sacrifices SQL compatibility. Not a big problem, but it becomes a bit unpredictable if this only happens on results. customersQueryable.Where(x=>x.Orders.Sum(y=>y.Amount)>=0) will not return customers with no orders. In a LINQ to Objects query, it would. I've handled this is my current project by introducing an expression visitor which rewrites all such queries, adding a Coalesce to the expression tree. It adds query cost, so it's not used everywhere. |
Good point. Well I will have a look to check if that is that hard to preserve this behavior. |
bb5a9fb
to
0c1bd20
Compare
Ok, done. At first it was looking easy, but I had discovered another polymorphic case still not supported : aggregation of non-nullable values when some concrete class have no rows when other was. I have fixed it. Now there is still a breaking change: when aggregating non nullable values while there is no rows for all concrete classes, it yields an exception, as expected. But previously, it was a GenericADOException having an ArgumentNullException as inner exception. Now it is an InvalidOperationException "Value cannot be null". |
// Leaving it untouched for allowing non polymorphic cases to work. | ||
break; | ||
case NhExpressionType.Max: | ||
AddPostExecuteTransformerForResultAggregate(ReflectionCache.QueryableMethods.MaxDefinition); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you use IQueryable<>
for aggregate methods? It does not make sense for me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The IntermediateHqlTree._postExecuteTransformers
is a list of LambdaExpression
which get merged together then compiled and called on IQueryable<T>
. As if they should be chainable, as if we it were useful and possible to add many of them, though the usages I have currently seen for them were all incompatible with chaining (they all yield another type than their input).
Yes that does not make sens to me either, that overly complicates things for defining the post execute transform (messing with expressions for runtime execution instead of just supplying directly an executable delegate), but there are no comments as for why such a design decision, and I have not decided to redesign this.
Redesign those transformers should imply considering the design of the list transformer, which works the same way and is way more used. But maybe this one has some cases where it could be translated to DB, though I doubt.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The IEnumerable<>
just works here.
|
||
if (elementType.IsNullableOrReference()) | ||
{ | ||
var inputListType = typeof(IQueryable<>).MakeGenericType(elementType); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, why IQueryable<>
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With my previous answer in mind, it would work with the IEnumerable
version too for current usage of the post execute transformer. I have made that choice for aligning with current input type of post execute transformers. True, there is an AsQueryable
call just for supplying that input type to it. I really do not understand what were the design rationals behind the way this post execute transformer currently works. It looks to me as lot of overhead for no added value. (Enable "merge" of execute transformer, but none of existing one seems mergeable to me. Maybe some serialization needs again. All that ends up in the query plan, which is serializable.)
With the current state of usage of this post execute transformer, it should works with IEnumerable
methods too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It requires Hm... there we also specify the method explicitly..AsQueryable()
/IQueryable<>
for some cases where the method is from the original query expression (See ProcessFirstOrSingleBase
, so for First, FirstOrDefault, Single, SingleOrDefault).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really do not understand what were the design rationale behind the way this post execute transformer currently works
I think it's just a copy of ListTransformer implementation, which is, indeed, chainable.
} | ||
|
||
// Non-reg case | ||
[Test, Ignore("Candidate potential breaking changes: Sum would now yield 0 instead of null on empty result sets.")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I removed Ignore
, test did pass.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's because the query isn't polymorphic
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rebase & conflict resolution miss, I had removed it prior to rebase. Going to remove it again.
var allCall = Expression.Call(allMethod, inputList, Expression.Lambda(noSumCondition, element)); | ||
|
||
Expression conditionalAggregateCall = Expression.Condition(allCall, | ||
Expression.Convert(Expression.Constant(null), concreteQueryElementType), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here we do not need a cast expression. Expression.Constant
has an overload which accepts a type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing it on the above constant too, better to compare both side having the same type.
0c1bd20
to
310fd7f
Compare
Changes done. Added some more comments in code too. |
// NH-3850: changed from list transformer (working on IEnumerable<object>) to post execute | ||
// transformer (working on IEnumerable<inputType>) for globally aggregating polymorphic results | ||
// instead of aggregating results for each class separately and yielding only the first. | ||
// If the aggregation relies on ordering, final result will still be wrong due to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the aggregation relies on ordering, final result will still be wrong
Can you demonstrate this with a test?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test added. By the way, hardened the non mutable seed test for it to be no more sensible to ordering. I prefer avoid relying on current polymorphic classes ordering whenever possible for tests, since it does look undocumented.
// a GenericADOException with an ArgumentNullException inner exception. | ||
|
||
var nullableElementType = typeof(Nullable<>).MakeGenericType(new[] { elementType }); | ||
var nullableInputListType = typeof(IQueryable<>).MakeGenericType(nullableElementType); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need to use IEnumerable<>
here (and everywhere else).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewing again some other parts of the fix, it appears to me I was not consistent with that. This fix has spanned a long period, I have ended up not remembering all of it. (That is the first NHibernate Jira I have started to work on.)
// This still causes a change: the expected failure case will yield an InvalidOperationException instead of | ||
// a GenericADOException with an ArgumentNullException inner exception. | ||
|
||
var nullableElementType = typeof(Nullable<>).MakeGenericType(new[] { elementType }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please do not use array creation in MakeGenericType
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for having not spotted the params
modifier. Added to the new ReflectionHelper.GetMethodOverload
by the way.
310fd7f
to
91c671f
Compare
Newer changes done. And removed some unneeded private setter, internal => private. |
Some additional considerations about those post result transformer: I have contemplated adding a This would avoid running this transformer on non polymorphic cases. But for handling polymorphic cases requiring a type override, this would require an additional Moreover, the main overhead is probably currently more in constructing this transformer than in using it. Now if we wanted to fix the ordering issue of polymorphic issue, we could try to extract the order by from the Linq query for reapplying it on union-ed polymorphic results. But then, re-ordering non polymorphic results would be a significant overhead without any value. There a |
After all these efforts I'm thinking about banning all implicit polymorphism with LINQ. Otherwise the cases needs to be supported as well (thru other JIRA & PR):
|
While ordering alone looks achievable, though already bad for performances (in-memory ordering required), paging support with ordering would be quite a monster for performances, especially for reaching far away pages. Depending on required order, the polymorphic page could have to pick elements ranging from row 0 up to "end of required page row number" in each concrete table. This would cause paging to be pointless. Polymorphism with other API are not in a better state, they do not support paging or ordering better. Indeed, polymorphic aggregates with HQL yields as many rows as concrete classes. So taking the first achieve the same result than current Linq. Trying Concrete results sets are just union-ed without ordering with HQL too. Linq an HQL run the same code for that. There is some code for trying a paging accounting polymorphism, but without handling the ordering issue, and as far as I remember, quite flawed (was not yielding pages of expected size I believe). (I have not checked the criteria/queryover cases.) So well, why not disabling it for all API if we decide to disable it for Linq? |
91c671f
to
f76cd93
Compare
Rebased for resolving conflicts with reflection refactoring. |
…egators failures: acknowledging cases which will not be fixed.
f76cd93
to
cdf2a7b
Compare
Fix some bad white-spaces I had inadvertently introduced, fix some comments and adjusted some reflection cache parameter specification syntax (using |
Fix and test cases for NH-3850.
Currently, LINQ queries such as
query.Count()
,query.Any()
do not support polymorphic queries: they take into account only the results of the first entity class matching the query and ignores the other classes results.This fix implements polymorphic support for
Aggregate
,Any
,Count
,LongCount
,Max
,Min
andSum
.Average
is not fixed: supporting it requires to select additional columns in the query (Sum
andCount
) and compute the average on client side. I do not think it is worth the hassle.This fix introduces a possible breaking change:
Sum
on nullables will no more yieldnull
when there is no rows to compute from, but will yield0
. This change is due to usage ofEnumerable.Sum
for aggregating polymorphic results, and this is its documented behavior. This happens even on non polymorphic queries: when adding theEnumerable.Sum
post result transformer, we do not already knows if the query will be polymorphic, so we add it to non polymorphic queries too. (Determining if the query is polymorphic would require some computation currently done in query plan computation.)Avoiding this change should be doable by building a more elaborated expression as post result transformer. But this changes looks to me very minor and not contradictory with Linq contracts. It causes the loss of the ability to tell from the result there was nothing to
Sum
, but that does not look to me as being part ofSum
contract. So I have not implemented a workaround for avoiding this change.