Skip to content

Commit 41bed92

Browse files
committed
Merge pull request #442 from PleasantD/NH-3816
NH-3816 - Fix Select with Conditional
2 parents 9e6d66b + 9a228c7 commit 41bed92

16 files changed

+362
-43
lines changed

src/NHibernate.Test/Linq/JoinTests.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,11 +273,13 @@ public void OrderLinesWithSelectingCustomerIdInCaseShouldProduceOneJoin()
273273
}
274274
}
275275

276-
[Test(Description = "NH-3801")]
276+
[Test(Description = "NH-3801"), Ignore("This is an ideal case, but not possible without better join detection")]
277277
public void OrderLinesWithSelectingCustomerInCaseShouldProduceOneJoin()
278278
{
279279
using (var spy = new SqlLogSpy())
280280
{
281+
// Without nominating the conditional to the select clause (and placing it in SQL)
282+
// [l.Order.Customer] will be selected in its entirety, creating a second join
281283
(from l in db.OrderLines
282284
select new { CustomerKnown = l.Order.Customer == null ? 0 : 1, l.Order.OrderDate }).ToList();
283285

@@ -299,11 +301,13 @@ public void OrderLinesWithSelectingCustomerNameInCaseShouldProduceTwoJoins()
299301
}
300302
}
301303

302-
[Test(Description = "NH-3801")]
304+
[Test(Description = "NH-3801"), Ignore("This is an ideal case, but not possible without better join detection")]
303305
public void OrderLinesWithSelectingCustomerNameInCaseShouldProduceTwoJoinsAlternate()
304306
{
305307
using (var spy = new SqlLogSpy())
306308
{
309+
// Without nominating the conditional to the select clause (and placing it in SQL)
310+
// [l.Order.Customer] will be selected in its entirety, creating a second join
307311
(from l in db.OrderLines
308312
select new { CustomerKnown = l.Order.Customer == null ? "unknown" : l.Order.Customer.CompanyName, l.Order.OrderDate }).ToList();
309313

src/NHibernate.Test/Linq/SelectionTests.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,76 @@ public void CanSelectCollection()
387387
Assert.AreEqual(5, orders[0].Count);
388388
}
389389

390+
[Test]
391+
public void CanSelectConditionalKnownTypes()
392+
{
393+
var moreThanTwoOrderLinesBool = db.Orders.Select(o => new { Id = o.OrderId, HasMoreThanTwo = o.OrderLines.Count() > 2 ? true : false }).ToList();
394+
Assert.That(moreThanTwoOrderLinesBool.Count(x => x.HasMoreThanTwo == true), Is.EqualTo(410));
395+
396+
var moreThanTwoOrderLinesNBool = db.Orders.Select(o => new { Id = o.OrderId, HasMoreThanTwo = o.OrderLines.Count() > 2 ? true : (bool?)null }).ToList();
397+
Assert.That(moreThanTwoOrderLinesNBool.Count(x => x.HasMoreThanTwo == true), Is.EqualTo(410));
398+
399+
var moreThanTwoOrderLinesShort = db.Orders.Select(o => new { Id = o.OrderId, HasMoreThanTwo = o.OrderLines.Count() > 2 ? (short)1 : (short)0 }).ToList();
400+
Assert.That(moreThanTwoOrderLinesShort.Count(x => x.HasMoreThanTwo == 1), Is.EqualTo(410));
401+
402+
var moreThanTwoOrderLinesNShort = db.Orders.Select(o => new { Id = o.OrderId, HasMoreThanTwo = o.OrderLines.Count() > 2 ? (short?)1 : (short?)null }).ToList();
403+
Assert.That(moreThanTwoOrderLinesNShort.Count(x => x.HasMoreThanTwo == 1), Is.EqualTo(410));
404+
405+
var moreThanTwoOrderLinesInt = db.Orders.Select(o => new { Id = o.OrderId, HasMoreThanTwo = o.OrderLines.Count() > 2 ? 1 : 0 }).ToList();
406+
Assert.That(moreThanTwoOrderLinesInt.Count(x => x.HasMoreThanTwo == 1), Is.EqualTo(410));
407+
408+
var moreThanTwoOrderLinesNInt = db.Orders.Select(o => new { Id = o.OrderId, HasMoreThanTwo = o.OrderLines.Count() > 2 ? 1 : (int?)null }).ToList();
409+
Assert.That(moreThanTwoOrderLinesNInt.Count(x => x.HasMoreThanTwo == 1), Is.EqualTo(410));
410+
411+
var moreThanTwoOrderLinesDecimal = db.Orders.Select(o => new { Id = o.OrderId, HasMoreThanTwo = o.OrderLines.Count() > 2 ? 1m : 0m }).ToList();
412+
Assert.That(moreThanTwoOrderLinesDecimal.Count(x => x.HasMoreThanTwo == 1m), Is.EqualTo(410));
413+
414+
var moreThanTwoOrderLinesNDecimal = db.Orders.Select(o => new { Id = o.OrderId, HasMoreThanTwo = o.OrderLines.Count() > 2 ? 1m : (decimal?)null }).ToList();
415+
Assert.That(moreThanTwoOrderLinesNDecimal.Count(x => x.HasMoreThanTwo == 1m), Is.EqualTo(410));
416+
417+
var moreThanTwoOrderLinesSingle = db.Orders.Select(o => new { Id = o.OrderId, HasMoreThanTwo = o.OrderLines.Count() > 2 ? 1f : 0f }).ToList();
418+
Assert.That(moreThanTwoOrderLinesSingle.Count(x => x.HasMoreThanTwo == 1f), Is.EqualTo(410));
419+
420+
var moreThanTwoOrderLinesNSingle = db.Orders.Select(o => new { Id = o.OrderId, HasMoreThanTwo = o.OrderLines.Count() > 2 ? 1f : (float?)null }).ToList();
421+
Assert.That(moreThanTwoOrderLinesNSingle.Count(x => x.HasMoreThanTwo == 1f), Is.EqualTo(410));
422+
423+
var moreThanTwoOrderLinesDouble = db.Orders.Select(o => new { Id = o.OrderId, HasMoreThanTwo = o.OrderLines.Count() > 2 ? 1d : 0d }).ToList();
424+
Assert.That(moreThanTwoOrderLinesDouble.Count(x => x.HasMoreThanTwo == 1d), Is.EqualTo(410));
425+
426+
var moreThanTwoOrderLinesNDouble = db.Orders.Select(o => new { Id = o.OrderId, HasMoreThanTwo = o.OrderLines.Count() > 2 ? 1d : (double?)null }).ToList();
427+
Assert.That(moreThanTwoOrderLinesNDouble.Count(x => x.HasMoreThanTwo == 1d), Is.EqualTo(410));
428+
429+
var moreThanTwoOrderLinesString = db.Orders.Select(o => new { Id = o.OrderId, HasMoreThanTwo = o.OrderLines.Count() > 2 ? "yes" : "no" }).ToList();
430+
Assert.That(moreThanTwoOrderLinesString.Count(x => x.HasMoreThanTwo == "yes"), Is.EqualTo(410));
431+
432+
var now = DateTime.Now.Date;
433+
var moreThanTwoOrderLinesDateTime = db.Orders.Select(o => new { Id = o.OrderId, HasMoreThanTwo = o.OrderLines.Count() > 2 ? o.OrderDate.Value : now }).ToList();
434+
Assert.That(moreThanTwoOrderLinesDateTime.Count(x => x.HasMoreThanTwo != now), Is.EqualTo(410));
435+
436+
var moreThanTwoOrderLinesNDateTime = db.Orders.Select(o => new { Id = o.OrderId, HasMoreThanTwo = o.OrderLines.Count() > 2 ? o.OrderDate : null }).ToList();
437+
Assert.That(moreThanTwoOrderLinesNDateTime.Count(x => x.HasMoreThanTwo != null), Is.EqualTo(410));
438+
439+
var moreThanTwoOrderLinesGuid = db.Orders.Select(o => new { Id = o.OrderId, HasMoreThanTwo = o.OrderLines.Count() > 2 ? o.Shipper.Reference : Guid.Empty }).ToList();
440+
Assert.That(moreThanTwoOrderLinesGuid.Count(x => x.HasMoreThanTwo != Guid.Empty), Is.EqualTo(410));
441+
442+
var moreThanTwoOrderLinesNGuid = db.Orders.Select(o => new { Id = o.OrderId, HasMoreThanTwo = o.OrderLines.Count() > 2 ? o.Shipper.Reference : (Guid?)null }).ToList();
443+
Assert.That(moreThanTwoOrderLinesNGuid.Count(x => x.HasMoreThanTwo != null), Is.EqualTo(410));
444+
}
445+
446+
[Test]
447+
public void CanSelectConditionalEntity()
448+
{
449+
var fatherInsteadOfChild = db.Animals.Select(a => a.Father.SerialNumber == "5678" ? a.Father : a).ToList();
450+
Assert.That(fatherInsteadOfChild, Has.Exactly(2).With.Property("SerialNumber").EqualTo("5678"));
451+
}
452+
453+
[Test]
454+
public void CanSelectConditionalObject()
455+
{
456+
var fatherIsKnown = db.Animals.Select(a => new { a.SerialNumber, Superior = a.Father.SerialNumber, FatherIsKnown = a.Father.SerialNumber == "5678" ? (object)true : (object)false }).ToList();
457+
Assert.That(fatherIsKnown, Has.Exactly(1).With.Property("FatherIsKnown").True);
458+
}
459+
390460
public class Wrapper<T>
391461
{
392462
public T item;
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
using System;
2+
using System.Linq;
3+
using System.Linq.Dynamic;
4+
using NHibernate.Linq;
5+
using NUnit.Framework;
6+
7+
namespace NHibernate.Test.NHSpecificTest.NH3818
8+
{
9+
[TestFixture]
10+
public class Fixture : BugTestCase
11+
{
12+
public override string BugNumber
13+
{
14+
get { return "NH3818"; }
15+
}
16+
17+
[Test]
18+
public void SelectConditionalValuesTest()
19+
{
20+
using (var spy = new SqlLogSpy())
21+
using (var session = OpenSession())
22+
using (session.BeginTransaction())
23+
{
24+
var days = 33;
25+
26+
var cat = new MyLovelyCat
27+
{
28+
GUID = Guid.NewGuid(),
29+
Birthdate = DateTime.Now.AddDays(-days),
30+
Color = "Black",
31+
Name = "Kitty",
32+
Price = 0
33+
};
34+
session.Save(cat);
35+
36+
session.Flush();
37+
38+
var catInfo =
39+
session.Query<MyLovelyCat>()
40+
.Select(o => new
41+
{
42+
o.Color,
43+
AliveDays = (int)(DateTime.Now - o.Birthdate).TotalDays,
44+
o.Name,
45+
o.Price,
46+
})
47+
.Single();
48+
49+
//Console.WriteLine(spy.ToString());
50+
Assert.That(catInfo.AliveDays == days);
51+
52+
var catInfo2 =
53+
session.Query<MyLovelyCat>()
54+
.Select(o => new
55+
{
56+
o.Color,
57+
AliveDays = o.Price > 0 ? (DateTime.Now - o.Birthdate).TotalDays : 0,
58+
o.Name,
59+
o.Price,
60+
})
61+
.Single();
62+
63+
//Console.WriteLine(spy.ToString());
64+
Assert.That(catInfo2.AliveDays == 0);
65+
66+
}
67+
}
68+
69+
}
70+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
3+
assembly="NHibernate.Test"
4+
namespace="NHibernate.Test.NHSpecificTest.NH3818"
5+
default-lazy="false"
6+
default-access="property">
7+
8+
<class name="MyLovelyCat" table="MyLovelyCat">
9+
<id column="GUID" name="GUID"></id>
10+
<property column="Name" name="Name" not-null="true"/>
11+
<property column="Color" name="Color" not-null="true"/>
12+
<property column="Birthdate" name="Birthdate" not-null="true"/>
13+
<property column="Price" name="Price" not-null="true"/>
14+
</class>
15+
16+
</hibernate-mapping>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
6+
namespace NHibernate.Test.NHSpecificTest.NH3818
7+
{
8+
class MyLovelyCat
9+
{
10+
public virtual Guid GUID { get; set; }
11+
12+
public virtual String Name { get; set; }
13+
public virtual String Color { get; set; }
14+
public virtual DateTime Birthdate { get; set; }
15+
public virtual Decimal Price { get; set; }
16+
}
17+
}

src/NHibernate.Test/NHibernate.Test.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -872,6 +872,8 @@
872872
<Compile Include="NHSpecificTest\NH3727\Entity.cs" />
873873
<Compile Include="NHSpecificTest\NH3727\FixtureByCode.cs" />
874874
<Compile Include="NHSpecificTest\NH3795\Fixture.cs" />
875+
<Compile Include="NHSpecificTest\NH3818\Fixture.cs" />
876+
<Compile Include="NHSpecificTest\NH3818\MyLovelyCat.cs" />
875877
<Compile Include="NHSpecificTest\NH646\Domain.cs" />
876878
<Compile Include="NHSpecificTest\NH646\Fixture.cs" />
877879
<Compile Include="NHSpecificTest\NH3234\Fixture.cs" />
@@ -3145,6 +3147,7 @@
31453147
<EmbeddedResource Include="NHSpecificTest\NH1291AnonExample\Mappings.hbm.xml" />
31463148
</ItemGroup>
31473149
<ItemGroup>
3150+
<EmbeddedResource Include="NHSpecificTest\NH3818\Mappings.hbm.xml" />
31483151
<EmbeddedResource Include="NHSpecificTest\NH3666\Mappings.hbm.xml">
31493152
<SubType>Designer</SubType>
31503153
</EmbeddedResource>

src/NHibernate/Linq/Expressions/NhExpressionType.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public enum NhExpressionType
99
Count,
1010
Distinct,
1111
New,
12-
Star
12+
Star,
13+
Nominator
1314
}
1415
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using System.Linq.Expressions;
2+
using Remotion.Linq.Clauses.Expressions;
3+
using Remotion.Linq.Parsing;
4+
5+
namespace NHibernate.Linq.Expressions
6+
{
7+
/// <summary>
8+
/// Represents an expression that has been nominated for direct inclusion in the SELECT clause.
9+
/// This bypasses the standard nomination process and assumes that the expression can be converted
10+
/// directly to SQL.
11+
/// </summary>
12+
/// <remarks>
13+
/// Used in the nomination of GroupBy key expressions to ensure that matching select clauses
14+
/// are generated the same way.
15+
/// </remarks>
16+
internal class NhNominatedExpression : ExtensionExpression
17+
{
18+
public Expression Expression { get; private set; }
19+
20+
public NhNominatedExpression(Expression expression) : base(expression.Type, (ExpressionType)NhExpressionType.Nominator)
21+
{
22+
Expression = expression;
23+
}
24+
25+
protected override Expression VisitChildren(ExpressionTreeVisitor visitor)
26+
{
27+
var newExpression = visitor.VisitExpression(Expression);
28+
29+
return newExpression != Expression
30+
? new NhNominatedExpression(newExpression)
31+
: this;
32+
}
33+
}
34+
}

src/NHibernate/Linq/GroupBy/GroupBySelectClauseRewriter.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@ public static Expression ReWrite(Expression expression, GroupResultOperator grou
2222

2323
private readonly GroupResultOperator _groupBy;
2424
private readonly QueryModel _model;
25+
private readonly Expression _nominatedKeySelector;
2526

2627
private GroupBySelectClauseRewriter(GroupResultOperator groupBy, QueryModel model)
2728
{
2829
_groupBy = groupBy;
2930
_model = model;
31+
_nominatedKeySelector = GroupKeyNominator.Visit(groupBy);
3032
}
3133

3234
protected override Expression VisitQuerySourceReferenceExpression(QuerySourceReferenceExpression expression)
@@ -53,7 +55,8 @@ protected override Expression VisitMemberExpression(MemberExpression expression)
5355

5456
if (expression.IsGroupingKeyOf(_groupBy))
5557
{
56-
return _groupBy.KeySelector;
58+
// If we have referenced the Key, then return the nominated key expression
59+
return _nominatedKeySelector;
5760
}
5861

5962
var elementSelector = _groupBy.ElementSelector;
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using System.Linq;
2+
using System.Linq.Expressions;
3+
using NHibernate.Linq.Expressions;
4+
using Remotion.Linq.Clauses.Expressions;
5+
using Remotion.Linq.Clauses.ResultOperators;
6+
using Remotion.Linq.Parsing;
7+
8+
namespace NHibernate.Linq.GroupBy
9+
{
10+
/// <summary>
11+
/// This class nominates sub-expression trees on the GroupBy Key expression
12+
/// for inclusion in the Select clause.
13+
/// </summary>
14+
internal class GroupKeyNominator : ExpressionTreeVisitor
15+
{
16+
private GroupKeyNominator() { }
17+
18+
private bool _requiresRootNomination;
19+
private bool _transformed;
20+
private int _depth;
21+
22+
public static Expression Visit(GroupResultOperator groupBy)
23+
{
24+
return VisitInternal(groupBy.KeySelector);
25+
}
26+
27+
private static Expression VisitInternal(Expression expr)
28+
{
29+
return new GroupKeyNominator().VisitExpression(expr);
30+
}
31+
32+
public override Expression VisitExpression(Expression expression)
33+
{
34+
_depth++;
35+
var expr = base.VisitExpression(expression);
36+
_depth--;
37+
38+
// At the root expression, wrap it in the nominator expression if needed
39+
if (_depth == 0 && !_transformed && _requiresRootNomination)
40+
{
41+
expr = new NhNominatedExpression(expr);
42+
}
43+
return expr;
44+
}
45+
46+
protected override Expression VisitNewArrayExpression(NewArrayExpression expression)
47+
{
48+
_transformed = true;
49+
// Transform each initializer recursively (to allow for nested initializers)
50+
return Expression.NewArrayInit(expression.Type.GetElementType(), expression.Expressions.Select(VisitInternal));
51+
}
52+
53+
protected override Expression VisitNewExpression(NewExpression expression)
54+
{
55+
_transformed = true;
56+
// Transform each initializer recursively (to allow for nested initializers)
57+
return Expression.New(expression.Constructor, expression.Arguments.Select(VisitInternal), expression.Members);
58+
}
59+
60+
protected override Expression VisitQuerySourceReferenceExpression(QuerySourceReferenceExpression expression)
61+
{
62+
// If the (sub)expression contains a QuerySourceReference, then the entire expression should be nominated
63+
_requiresRootNomination = true;
64+
return base.VisitQuerySourceReferenceExpression(expression);
65+
}
66+
67+
protected override Expression VisitSubQueryExpression(SubQueryExpression expression)
68+
{
69+
// If the (sub)expression contains a QuerySourceReference, then the entire expression should be nominated
70+
_requiresRootNomination = true;
71+
return base.VisitSubQueryExpression(expression);
72+
}
73+
}
74+
}

0 commit comments

Comments
 (0)