Skip to content

Commit fbe1ddc

Browse files
committed
NH-3474 - Fix support of GroupBy with constant keys
* Remove the GROUP BY clause entirely if all keys are constant * Re-write the GROUP BY clause using only non-constant keys * Adjust the ExpressionKeyVisitor so that constants in GroupBy are not replaced with parameter * Fix ExpressionKeyVisitor handling of constant arrays to show initialization values
1 parent 99e0bfc commit fbe1ddc

File tree

5 files changed

+151
-0
lines changed

5 files changed

+151
-0
lines changed

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

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,100 @@ public void GroupByComputedValueInObjectArray()
549549
Assert.AreEqual(830, orderGroups.Sum(g => g.Count));
550550
}
551551

552+
[Test(Description = "NH-3474")]
553+
public void GroupByConstant()
554+
{
555+
var totals = db.Orders.GroupBy(o => 1).Select(g => new { Key = g.Key, Count = g.Count(), Sum = g.Sum(x => x.Freight) }).ToList();
556+
Assert.That(totals.Count, Is.EqualTo(1));
557+
Assert.That(totals, Has.All.With.Property("Key").EqualTo(1));
558+
}
559+
560+
[Test(Description = "NH-3474")]
561+
public void GroupByConstantAnonymousType()
562+
{
563+
var totals = db.Orders.GroupBy(o => new { A = 1 }).Select(g => new { Key = g.Key, Count = g.Count(), Sum = g.Sum(x => x.Freight) }).ToList();
564+
Assert.That(totals.Count, Is.EqualTo(1));
565+
Assert.That(totals, Has.All.With.Property("Key").With.Property("A").EqualTo(1));
566+
}
567+
568+
[Test(Description = "NH-3474")]
569+
public void GroupByConstantArray()
570+
{
571+
var totals = db.Orders.GroupBy(o => new object[] { 1 }).Select(g => new { Key = g.Key, Count = g.Count(), Sum = g.Sum(x => x.Freight) }).ToList();
572+
Assert.That(totals.Count, Is.EqualTo(1));
573+
Assert.That(totals, Has.All.With.Property("Key").EqualTo(new object[] { 1 }));
574+
}
575+
576+
[Test(Description = "NH-3474")]
577+
public void GroupByKeyWithConstantInAnonymousType()
578+
{
579+
var totals = db.Orders.GroupBy(o => new { A = 1, B = o.Shipper.ShipperId }).Select(g => new { Key = g.Key, Count = g.Count(), Sum = g.Sum(x => x.Freight) }).ToList();
580+
Assert.That(totals.Count, Is.EqualTo(3));
581+
Assert.That(totals, Has.All.With.Property("Key").With.Property("A").EqualTo(1));
582+
}
583+
584+
[Test(Description = "NH-3474")]
585+
public void GroupByKeyWithConstantInArray()
586+
{
587+
var totals = db.Orders.GroupBy(o => new[] { 1, o.Shipper.ShipperId }).Select(g => new { Key = g.Key, Count = g.Count(), Sum = g.Sum(x => x.Freight) }).ToList();
588+
Assert.That(totals.Count, Is.EqualTo(3));
589+
Assert.That(totals, Has.All.With.Property("Key").Contains(1));
590+
}
591+
592+
private int constKey;
593+
[Test(Description = "NH-3474")]
594+
public void GroupByKeyWithConstantFromVariable()
595+
{
596+
constKey = 1;
597+
var q1 = db.Orders.GroupBy(o => constKey).Select(g => new {Key = g.Key, Count = g.Count(), Sum = g.Sum(x => x.Freight)});
598+
var q2 = db.Orders.GroupBy(o => new {A = constKey}).Select(g => new {Key = g.Key, Count = g.Count(), Sum = g.Sum(x => x.Freight)});
599+
var q3 = db.Orders.GroupBy(o => new object[] {constKey}).Select(g => new {Key = g.Key, Count = g.Count(), Sum = g.Sum(x => x.Freight)});
600+
var q4 = db.Orders.GroupBy(o => new {A = constKey, B = o.Shipper.ShipperId}).Select(g => new {Key = g.Key, Count = g.Count(), Sum = g.Sum(x => x.Freight)});
601+
var q5 = db.Orders.GroupBy(o => new[] {constKey, o.Shipper.ShipperId}).Select(g => new {Key = g.Key, Count = g.Count(), Sum = g.Sum(x => x.Freight)});
602+
603+
var r1_1 = q1.ToList();
604+
Assert.That(r1_1.Count, Is.EqualTo(1));
605+
Assert.That(r1_1, Has.All.With.Property("Key").EqualTo(1));
606+
607+
var r2_1 = q2.ToList();
608+
Assert.That(r2_1.Count, Is.EqualTo(1));
609+
Assert.That(r2_1, Has.All.With.Property("Key").With.Property("A").EqualTo(1));
610+
611+
var r3_1 = q3.ToList();
612+
Assert.That(r3_1.Count, Is.EqualTo(1));
613+
Assert.That(r3_1, Has.All.With.Property("Key").EqualTo(new object[] { 1 }));
614+
615+
var r4_1 = q4.ToList();
616+
Assert.That(r4_1.Count, Is.EqualTo(3));
617+
Assert.That(r4_1, Has.All.With.Property("Key").With.Property("A").EqualTo(1));
618+
619+
var r5_1 = q5.ToList();
620+
Assert.That(r5_1.Count, Is.EqualTo(3));
621+
Assert.That(r5_1, Has.All.With.Property("Key").Contains(1));
622+
623+
constKey = 2;
624+
625+
var r1_2 = q1.ToList();
626+
Assert.That(r1_2.Count, Is.EqualTo(1));
627+
Assert.That(r1_2, Has.All.With.Property("Key").EqualTo(2));
628+
629+
var r2_2 = q2.ToList();
630+
Assert.That(r2_2.Count, Is.EqualTo(1));
631+
Assert.That(r2_2, Has.All.With.Property("Key").With.Property("A").EqualTo(2));
632+
633+
var r3_2 = q3.ToList();
634+
Assert.That(r3_2.Count, Is.EqualTo(1));
635+
Assert.That(r3_2, Has.All.With.Property("Key").EqualTo(new object[] { constKey }));
636+
637+
var r4_2 = q4.ToList();
638+
Assert.That(r4_2.Count, Is.EqualTo(3));
639+
Assert.That(r4_2, Has.All.With.Property("Key").With.Property("A").EqualTo(2));
640+
641+
var r5_2 = q5.ToList();
642+
Assert.That(r5_2.Count, Is.EqualTo(3));
643+
Assert.That(r5_2, Has.All.With.Property("Key").Contains(2));
644+
}
645+
552646
[Test(Description = "NH-3801")]
553647
public void GroupByComputedValueWithJoinOnObject()
554648
{

src/NHibernate/Linq/GroupBy/AggregatingGroupByRewriter.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4+
using System.Linq.Expressions;
45
using NHibernate.Linq.Clauses;
56
using NHibernate.Linq.ReWriters;
67
using NHibernate.Linq.Visitors;
@@ -58,6 +59,7 @@ public static void ReWrite(QueryModel queryModel)
5859
if (groupBy != null)
5960
{
6061
FlattenSubQuery(queryModel, subQueryExpression.QueryModel, groupBy);
62+
RemoveCostantGroupByKeys(queryModel, groupBy);
6163
}
6264
}
6365
}
@@ -101,5 +103,22 @@ private static void FlattenSubQuery(QueryModel queryModel, QueryModel subQueryMo
101103
// Replace the outer query source
102104
queryModel.MainFromClause = subQueryModel.MainFromClause;
103105
}
106+
107+
private static void RemoveCostantGroupByKeys(QueryModel queryModel, GroupResultOperator groupBy)
108+
{
109+
var keys = groupBy.ExtractKeyExpressions().Where(x => !(x is ConstantExpression)).ToList();
110+
111+
if (!keys.Any())
112+
{
113+
// Remove the Group By clause completely if all the keys are constant (redundant)
114+
queryModel.ResultOperators.Remove(groupBy);
115+
}
116+
else
117+
{
118+
// Re-write the KeySelector as an object array of the non-constant keys
119+
// This should be safe because we've already re-written the select clause using the original keys
120+
groupBy.KeySelector = Expression.NewArrayInit(typeof (object), keys.Select(x => x.Type.IsValueType ? Expression.Convert(x, typeof(object)) : x));
121+
}
122+
}
104123
}
105124
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Linq.Expressions;
5+
using System.Text;
6+
using NHibernate.Util;
7+
using Remotion.Linq.Clauses.ResultOperators;
8+
9+
namespace NHibernate.Linq
10+
{
11+
internal static class GroupResultOperatorExtensions
12+
{
13+
public static IEnumerable<Expression> ExtractKeyExpressions(this GroupResultOperator groupResult)
14+
{
15+
if (groupResult.KeySelector is NewExpression)
16+
return (groupResult.KeySelector as NewExpression).Arguments;
17+
if (groupResult.KeySelector is NewArrayExpression)
18+
return (groupResult.KeySelector as NewArrayExpression).Expressions;
19+
return new [] { groupResult.KeySelector };
20+
}
21+
}
22+
}

src/NHibernate/Linq/Visitors/ExpressionKeyVisitor.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Collections;
23
using System.Collections.Generic;
34
using System.Linq;
@@ -92,9 +93,22 @@ protected override Expression VisitConstantExpression(ConstantExpression express
9293
else
9394
{
9495
if (expression.Value == null)
96+
{
9597
_string.Append("NULL");
98+
}
99+
else if (expression.Type.IsArray)
100+
{
101+
// Const arrays all look the same (they just display the type of array and not the initializer contents
102+
// Since the contents might be different, we need to include those as well so we don't use a cached query plan by mistake
103+
_string.Append(expression.Value);
104+
_string.Append(" {");
105+
_string.Append(String.Join(",", (object[]) expression.Value));
106+
_string.Append("}");
107+
}
96108
else
109+
{
97110
_string.Append(expression.Value);
111+
}
98112
}
99113

100114
return base.VisitConstantExpression(expression);
@@ -142,6 +156,7 @@ protected override Expression VisitMethodCallExpression(MethodCallExpression exp
142156
case "Single":
143157
case "SingleOrDefault":
144158
case "Select":
159+
case "GroupBy":
145160
insideSelectClause = true;
146161
break;
147162
default:

src/NHibernate/NHibernate.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@
299299
<Compile Include="Linq\Functions\EqualsGenerator.cs" />
300300
<Compile Include="Linq\GroupBy\KeySelectorVisitor.cs" />
301301
<Compile Include="Linq\GroupBy\PagingRewriter.cs" />
302+
<Compile Include="Linq\GroupResultOperatorExtensions.cs" />
302303
<Compile Include="Linq\NestedSelects\NestedSelectDetector.cs" />
303304
<Compile Include="Linq\NestedSelects\Tuple.cs" />
304305
<Compile Include="Linq\NestedSelects\SelectClauseRewriter.cs" />

0 commit comments

Comments
 (0)