Skip to content

Commit 12e8615

Browse files
committed
Merge branch 'NH-2380'
2 parents 4c38acb + 590c20f commit 12e8615

File tree

6 files changed

+204
-23
lines changed

6 files changed

+204
-23
lines changed

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ public void CountDistinctProperty_ReturnsNumberOfDistinctEntriesForThatProperty(
2222
.Distinct()
2323
.Count();
2424

25-
Console.WriteLine(result);
26-
2725
Assert.That(result, Is.EqualTo(387));
2826
}
2927

@@ -35,8 +33,6 @@ public void CountProperty_ReturnsNumberOfNonNullEntriesForThatProperty()
3533
.Select(x => x.ShippingDate)
3634
.Count();
3735

38-
Console.WriteLine(result);
39-
4036
Assert.That(result, Is.EqualTo(809));
4137
}
4238

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
using System;
2+
using System.Linq;
3+
using NHibernate.Cfg;
4+
using NUnit.Framework;
5+
using SharpTestsEx;
6+
7+
namespace NHibernate.Test.Linq.ByMethod
8+
{
9+
[TestFixture]
10+
public class DistinctTests : LinqTestCase
11+
{
12+
public class OrderDto
13+
{
14+
public string ShipCountry { get; set; }
15+
public DateTime? ShippingDate { get; set; }
16+
public DateTime? OrderDate { get; set; }
17+
}
18+
19+
private static T Transform<T>(T value)
20+
{
21+
return value;
22+
}
23+
24+
[Test]
25+
public void DistinctOnAnonymousTypeProjection()
26+
{
27+
//NH-2380
28+
var result = db.Orders
29+
.Select(x => new {x.ShippingDate})
30+
.Distinct()
31+
.ToArray();
32+
33+
result.Length.Should().Be.EqualTo(388);
34+
}
35+
36+
[Test]
37+
public void DistinctOnComplexAnonymousTypeProjection()
38+
{
39+
//NH-2380
40+
var result = db.Orders
41+
.Select(x => new
42+
{
43+
x.ShippingDate,
44+
x.OrderDate
45+
})
46+
.Distinct()
47+
.ToArray();
48+
49+
result.Length.Should().Be.EqualTo(774);
50+
}
51+
52+
[Test]
53+
public void DistinctOnTypeProjection()
54+
{
55+
//NH-2486
56+
OrderDto[] result = db.Orders
57+
.Select(x => new OrderDto
58+
{
59+
ShippingDate = x.ShippingDate
60+
})
61+
.Distinct()
62+
.ToArray();
63+
64+
result.Length.Should().Be.EqualTo(388);
65+
}
66+
67+
[Test]
68+
public void DistinctOnTypeProjectionTwoProperty()
69+
{
70+
//NH-2486
71+
OrderDto[] result = db.Orders
72+
.Select(x => new OrderDto
73+
{
74+
ShippingDate = x.ShippingDate,
75+
OrderDate = x.OrderDate
76+
})
77+
.Distinct()
78+
.ToArray();
79+
80+
result.Length.Should().Be.EqualTo(774);
81+
}
82+
83+
[Test]
84+
public void DistinctOnTypeProjectionWithHqlMethodIsOk()
85+
{
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+
107+
OrderDto[] result = db.Orders
108+
.Select(x => new OrderDto
109+
{
110+
ShippingDate = Transform(x.ShippingDate),
111+
OrderDate = Transform(x.OrderDate)
112+
})
113+
.Distinct()
114+
.ToArray();
115+
}
116+
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();
132+
}
133+
}
134+
}

src/NHibernate.Test/NHibernate.Test.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,7 @@
684684
<Compile Include="NHSpecificTest\AccessAndCorrectPropertyName\Model.cs" />
685685
<Compile Include="NHSpecificTest\BagWithLazyExtraAndFilter\Domain.cs" />
686686
<Compile Include="NHSpecificTest\BagWithLazyExtraAndFilter\Fixture.cs" />
687+
<Compile Include="Linq\ByMethod\DistinctTests.cs" />
687688
<Compile Include="Component\Basic\ComponentWithUniqueConstraintTests.cs" />
688689
<Compile Include="NHSpecificTest\NH1082\SynchronizationThatThrowsExceptionAtBeforeTransactionCompletion.cs" />
689690
<Compile Include="NHSpecificTest\NH2756\Fixture.cs" />

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: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,28 @@
1+
using System;
12
using System.Collections.Generic;
23
using System.Linq;
34
using System.Linq.Expressions;
45
using NHibernate.Hql.Ast;
6+
using NHibernate.Linq.Expressions;
57
using Remotion.Linq.Parsing;
68

79
namespace NHibernate.Linq.Visitors
810
{
911
public class SelectClauseVisitor : ExpressionTreeVisitor
1012
{
13+
private readonly HqlTreeBuilder _hqlTreeBuilder = new HqlTreeBuilder();
1114
private HashSet<Expression> _hqlNodes;
1215
private readonly ParameterExpression _inputParameter;
1316
private readonly VisitorParameters _parameters;
1417
private int _iColumn;
1518
private List<HqlExpression> _hqlTreeNodes = new List<HqlExpression>();
19+
private readonly HqlGeneratorExpressionTreeVisitor _hqlVisitor;
1620

1721
public SelectClauseVisitor(System.Type inputType, VisitorParameters parameters)
1822
{
1923
_inputParameter = Expression.Parameter(inputType, "input");
2024
_parameters = parameters;
25+
_hqlVisitor = new HqlGeneratorExpressionTreeVisitor(_parameters);
2126
}
2227

2328
public LambdaExpression ProjectionExpression { get; private set; }
@@ -29,19 +34,41 @@ public IEnumerable<HqlExpression> GetHqlNodes()
2934

3035
public void Visit(Expression expression)
3136
{
32-
// First, find the sub trees that can be expressed purely in HQL
33-
_hqlNodes = new SelectClauseHqlNominator(_parameters).Nominate(expression);
37+
var distinct = expression as NhDistinctExpression;
38+
if (distinct != null)
39+
{
40+
expression = distinct.Expression;
41+
}
42+
43+
// Find the sub trees that can be expressed purely in HQL
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.");
3454

3555
// Now visit the tree
36-
Expression projection = VisitExpression(expression);
56+
var projection = VisitExpression(expression);
3757

3858
if ((projection != expression) && !_hqlNodes.Contains(expression))
3959
{
4060
ProjectionExpression = Expression.Lambda(projection, _inputParameter);
4161
}
4262

43-
// Finally, handle any boolean results in the output nodes
63+
// Handle any boolean results in the output nodes
4464
_hqlTreeNodes = BooleanToCaseConvertor.Convert(_hqlTreeNodes).ToList();
65+
66+
if (distinct != null)
67+
{
68+
var treeNodes = new List<HqlTreeNode>(_hqlTreeNodes.Count + 1) {_hqlTreeBuilder.Distinct()};
69+
treeNodes.AddRange(_hqlTreeNodes);
70+
_hqlTreeNodes = new List<HqlExpression>(1) {_hqlTreeBuilder.ExpressionSubTreeHolder(treeNodes)};
71+
}
4572
}
4673

4774
public override Expression VisitExpression(Expression expression)
@@ -53,13 +80,10 @@ public override Expression VisitExpression(Expression expression)
5380

5481
if (_hqlNodes.Contains(expression))
5582
{
56-
// Pure HQL evaluation - TODO - cache the Visitor?
57-
var hqlVisitor = new HqlGeneratorExpressionTreeVisitor(_parameters);
58-
59-
_hqlTreeNodes.Add(hqlVisitor.Visit(expression).AsExpression());
83+
// Pure HQL evaluation
84+
_hqlTreeNodes.Add(_hqlVisitor.Visit(expression).AsExpression());
6085

61-
return Expression.Convert(
62-
Expression.ArrayIndex(_inputParameter, Expression.Constant(_iColumn++)), expression.Type);
86+
return Expression.Convert(Expression.ArrayIndex(_inputParameter, Expression.Constant(_iColumn++)), expression.Type);
6387
}
6488

6589
// Can't handle this node with HQL. Just recurse down, and emit the expression

src/NHibernate/Persister/Collection/AbstractCollectionPersister.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -853,8 +853,8 @@ private SqlString GenerateSelectSizeString(ISessionImplementor sessionImplemento
853853
protected virtual string GetCountSqlSelectClause()
854854
{
855855
// NH: too many "if" when each collection can have its persister
856-
if (isCollectionIntegerIndex) return string.Format("coalesce(max({0}) + 1, 0)", IndexColumnNames[0]); // Do we need this "optimization"?
857-
return string.Format("count({0})", HasIndex ? GetIndexCountExpression() : ElementColumnNames[0]);
856+
if (isCollectionIntegerIndex) return string.Format("coalesce(max({0}) + 1, 0)", IndexColumnNames[0]); // Do we need this "optimization"?
857+
return string.Format("count({0})", HasIndex ? GetIndexCountExpression() : ElementColumnNames[0]);
858858
}
859859

860860
private string GetIndexCountExpression()

0 commit comments

Comments
 (0)