Skip to content

Commit 590c20f

Browse files
oskarbhazzik
authored andcommitted
LINQ: Prevent using server-side Distinct() together with projection methods executed locally, since the distinct would operate on the values before the local method is called.
1 parent 14b7740 commit 590c20f

File tree

3 files changed

+83
-11
lines changed

3 files changed

+83
-11
lines changed

src/NHibernate.Test/Linq/ByMethod/DistinctTests.cs

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ public class DistinctTests : LinqTestCase
1111
{
1212
public class OrderDto
1313
{
14+
public string ShipCountry { get; set; }
1415
public DateTime? ShippingDate { get; set; }
1516
public DateTime? OrderDate { get; set; }
1617
}
@@ -80,9 +81,29 @@ public void DistinctOnTypeProjectionTwoProperty()
8081
}
8182

8283
[Test]
83-
public void DistinctOnTypeProjectionWithCustomProjectionMethods()
84+
public void DistinctOnTypeProjectionWithHqlMethodIsOk()
8485
{
85-
//NH-2645
86+
// Sort of related to NH-2645.
87+
88+
OrderDto[] result = db.Orders
89+
.Select(x => new OrderDto
90+
{
91+
ShipCountry = x.ShippingAddress.Country.ToLower(), // Should be translated to HQL/SQL.
92+
ShippingDate = x.ShippingDate,
93+
OrderDate = x.OrderDate.Value.Date,
94+
})
95+
.Distinct()
96+
.ToArray();
97+
98+
result.Length.Should().Be.EqualTo(824);
99+
}
100+
101+
[Test]
102+
[ExpectedException(typeof(NotSupportedException), ExpectedMessage = "Cannot use distinct on result that depends on methods for which no SQL equivalent exist.")]
103+
public void DistinctOnTypeProjectionWithCustomProjectionMethodsIsBlocked1()
104+
{
105+
// Sort of related to NH-2645.
106+
86107
OrderDto[] result = db.Orders
87108
.Select(x => new OrderDto
88109
{
@@ -91,8 +112,23 @@ public void DistinctOnTypeProjectionWithCustomProjectionMethods()
91112
})
92113
.Distinct()
93114
.ToArray();
115+
}
94116

95-
result.Length.Should().Be.EqualTo(774);
117+
118+
[Test]
119+
[ExpectedException(typeof(NotSupportedException), ExpectedMessage = "Cannot use distinct on result that depends on methods for which no SQL equivalent exist.")]
120+
public void DistinctOnTypeProjectionWithCustomProjectionMethodsIsBlocked2()
121+
{
122+
// Sort of related to NH-2645.
123+
124+
OrderDto[] result = db.Orders
125+
.Select(x => new OrderDto
126+
{
127+
ShippingDate = x.ShippingDate,
128+
OrderDate = x.OrderDate.Value.AddMonths(5), // As of 2012-01-25, AddMonths() is executed locally.
129+
})
130+
.Distinct()
131+
.ToArray();
96132
}
97133
}
98134
}

src/NHibernate/Linq/Visitors/SelectClauseNominator.cs

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,27 @@
66

77
namespace NHibernate.Linq.Visitors
88
{
9+
/// <summary>
10+
/// Analyze the select clause to determine what parts can be translated
11+
/// fully to HQL, and some other properties of the clause.
12+
/// </summary>
913
class SelectClauseHqlNominator : ExpressionTreeVisitor
1014
{
1115
private readonly ILinqToHqlGeneratorsRegistry _functionRegistry;
1216

13-
private HashSet<Expression> _candidates;
17+
/// <summary>
18+
/// The expression parts that can be converted to pure HQL.
19+
/// </summary>
20+
public HashSet<Expression> HqlCandidates { get; private set; }
21+
22+
/// <summary>
23+
/// If true after an expression have been analyzed, the
24+
/// expression as a whole contain at least one method call which
25+
/// cannot be converted to a registered function, i.e. it must
26+
/// be executed client side.
27+
/// </summary>
28+
public bool ContainsUntranslatedMethodCalls { get; private set; }
29+
1430
private bool _canBeCandidate;
1531
Stack<bool> _stateStack;
1632

@@ -19,16 +35,15 @@ public SelectClauseHqlNominator(VisitorParameters parameters)
1935
_functionRegistry = parameters.SessionFactory.Settings.LinqToHqlGeneratorsRegistry;
2036
}
2137

22-
internal HashSet<Expression> Nominate(Expression expression)
38+
internal void Visit(Expression expression)
2339
{
24-
_candidates = new HashSet<Expression>();
40+
HqlCandidates = new HashSet<Expression>();
41+
ContainsUntranslatedMethodCalls = false;
2542
_canBeCandidate = true;
2643
_stateStack = new Stack<bool>();
2744
_stateStack.Push(false);
2845

2946
VisitExpression(expression);
30-
31-
return _candidates;
3247
}
3348

3449
public override Expression VisitExpression(Expression expression)
@@ -38,6 +53,17 @@ public override Expression VisitExpression(Expression expression)
3853
var projectConstantsInHql = _stateStack.Peek() ||
3954
expression != null && IsRegisteredFunction(expression);
4055

56+
// Set some flags, unless we already have proper values for them:
57+
// projectConstantsInHql if they are inside a method call executed server side.
58+
// ContainsUntranslatedMethodCalls if a method call must be executed locally.
59+
var isMethodCall = expression != null && expression.NodeType == ExpressionType.Call;
60+
if (isMethodCall && (!projectConstantsInHql || !ContainsUntranslatedMethodCalls))
61+
{
62+
var isRegisteredFunction = IsRegisteredFunction(expression);
63+
projectConstantsInHql = projectConstantsInHql || isRegisteredFunction;
64+
ContainsUntranslatedMethodCalls = ContainsUntranslatedMethodCalls || !isRegisteredFunction;
65+
}
66+
4167
_stateStack.Push(projectConstantsInHql);
4268

4369
if (expression == null)
@@ -49,7 +75,7 @@ public override Expression VisitExpression(Expression expression)
4975

5076
if (CanBeEvaluatedInHqlStatementShortcut(expression))
5177
{
52-
_candidates.Add(expression);
78+
HqlCandidates.Add(expression);
5379
return expression;
5480
}
5581

@@ -59,7 +85,7 @@ public override Expression VisitExpression(Expression expression)
5985
{
6086
if (CanBeEvaluatedInHqlSelectStatement(expression, projectConstantsInHql))
6187
{
62-
_candidates.Add(expression);
88+
HqlCandidates.Add(expression);
6389
}
6490
else
6591
{

src/NHibernate/Linq/Visitors/SelectClauseVisitor.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Collections.Generic;
23
using System.Linq;
34
using System.Linq.Expressions;
@@ -40,7 +41,16 @@ public void Visit(Expression expression)
4041
}
4142

4243
// Find the sub trees that can be expressed purely in HQL
43-
_hqlNodes = new SelectClauseHqlNominator(_parameters).Nominate(expression);
44+
var nominator = new SelectClauseHqlNominator(_parameters);
45+
nominator.Visit(expression);
46+
_hqlNodes = nominator.HqlCandidates;
47+
48+
// Linq2SQL ignores calls to local methods. Linq2EF seems to not support
49+
// calls to local methods at all. For NHibernate we support local methods,
50+
// but prevent their use together with server-side distinct, since it may
51+
// end up being wrong.
52+
if (distinct != null && nominator.ContainsUntranslatedMethodCalls)
53+
throw new NotSupportedException("Cannot use distinct on result that depends on methods for which no SQL equivalent exist.");
4454

4555
// Now visit the tree
4656
var projection = VisitExpression(expression);

0 commit comments

Comments
 (0)