Skip to content

Commit 2a8c46e

Browse files
Make SUM never be NULL (#267)
1 parent 210746a commit 2a8c46e

7 files changed

Lines changed: 145 additions & 32 deletions

File tree

net/DevExtreme.AspNet.Data.Tests.Common/SummaryTestHelper.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ public static void Run<T>(IQueryable<T> data) where T : IEntity {
7171
Assert.Equal(new object[] { 2, 1, 1, 1m, 1m }, group_A_A.summary);
7272
Assert.Equal(new object[] { 2, 3, 5, 8m, 4m }, group_A_B.summary);
7373

74-
Assert.Equal(new object[] { 1, null, null, null, null }, group_B.summary);
75-
Assert.Equal(new object[] { 1, null, null, null, null }, group_B_A.summary);
74+
Assert.Equal(new object[] { 1, null, null, 0m, null }, group_B.summary);
75+
Assert.Equal(new object[] { 1, null, null, 0m, null }, group_B_A.summary);
7676

7777
Assert.Equal(new object[] { 5, 1, 5, 9m, 3m }, loadResult.summary);
7878
}
@@ -81,7 +81,7 @@ public static void Run<T>(IQueryable<T> data) where T : IEntity {
8181

8282
{
8383
var loadResult = DataSourceLoader.Load(data, loadOptions);
84-
Assert.Equal(new object[] { 0, null, null, null, null }, loadResult.summary);
84+
Assert.Equal(new object[] { 0, null, null, 0m, null }, loadResult.summary);
8585
}
8686
}
8787

@@ -94,11 +94,12 @@ public static void Run<T>(IQueryable<T> data) where T : IEntity {
9494
B Count=2, Min=3, Max=5, Sum=8, Avg=4
9595
3
9696
5
97-
B Count=1, Min=N, Max=N, Sum=N, Avg=N
98-
A Count=1, Min=N, Max=N, Sum=N, Avg=N
97+
B Count=1, Min=N, Max=N, Sum=0*, Avg=N
98+
A Count=1, Min=N, Max=N, Sum=0*, Avg=N
9999
N
100100
101101
TOTALS: Count=5, Min=1, Max=5, Sum=9, Avg=3
102+
* - see SumFix
102103
*/
103104

104105
}

net/DevExtreme.AspNet.Data.Tests/AggregateCalculatorTests.cs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ public void Calculation_Empty() {
8181
new object[0],
8282
// SQL: sum=N min=N max=N avg=N count=0
8383

84-
expectedSum: null,
84+
expectedSum: 0m, // SumFix
8585
expectedMin: null,
8686
expectedMax: null,
8787
expectedAvg: null,
@@ -112,7 +112,7 @@ public void Calculation_Nulls() {
112112
new object[] { null },
113113
// SQL: sum=N min=N max=N avg=N count=1
114114

115-
expectedSum: null,
115+
expectedSum: 0m, // SumFix
116116
expectedMin: null,
117117
expectedMax: null,
118118
expectedAvg: null,
@@ -239,6 +239,46 @@ public void TimeSpanType() {
239239
);
240240
}
241241

242+
[Fact]
243+
public void SumFix() {
244+
var summary = Enumerable.Range(1, 4)
245+
.Select(i => new SummaryInfo { SummaryType = "sum", Selector = "Item" + i })
246+
.ToArray();
247+
248+
var data = new[] {
249+
new Group {
250+
items = new[] { new SumFixItem() }
251+
},
252+
new Group {
253+
items = new object[] { null }
254+
},
255+
new Group {
256+
items = Array.Empty<object>()
257+
}
258+
};
259+
260+
var totals = new AggregateCalculator<SumFixItem>(data, new DefaultAccessor<SumFixItem>(), summary, summary).Run();
261+
262+
foreach(var values in new[] {
263+
totals,
264+
data[0].summary,
265+
data[1].summary,
266+
data[2].summary
267+
}) {
268+
Assert.Equal(0m, values[0]);
269+
Assert.Equal(0d, values[1]);
270+
Assert.Equal(default(TimeSpan), values[2]);
271+
Assert.Equal(0m, values[3]);
272+
}
273+
}
274+
275+
class SumFixItem {
276+
public short? Item1 { get; set; }
277+
public float? Item2 { get; set; }
278+
public TimeSpan? Item3 { get; set; }
279+
public object Item4 { get; set; }
280+
}
281+
242282
[Fact]
243283
public void CustomAggregator() {
244284
CustomAggregatorsBarrier.Run(delegate {

net/DevExtreme.AspNet.Data.Tests/RemoteGroupingTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ public void Summary_Empty() {
327327
});
328328

329329
Assert.Equal(
330-
new object[] { null, null, null, null, 0 },
330+
new object[] { 0m /* SumFix */, null, null, null, 0 },
331331
loadResult.summary
332332
);
333333
}
@@ -338,6 +338,7 @@ public void Summary_Average_EmptyWithZeroSum() {
338338
// https://github.com/aspnet/EntityFrameworkCore/issues/12307
339339

340340
var result = RemoteGrouping.RemoteGroupTransformer.Run(
341+
typeof(Object),
341342
new[] { new Types.AnonType<int, int, int, int>(
342343
0, // count
343344
0, // sum

net/DevExtreme.AspNet.Data/Aggregation/AggregateCalculator.cs

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,37 +12,36 @@ namespace DevExtreme.AspNet.Data.Aggregation {
1212
class AggregateCalculator<T> {
1313
IEnumerable _data;
1414
IAccessor<T> _accessor;
15+
SumFix _sumFix;
16+
IList<SummaryInfo> _totalSummary;
17+
IList<SummaryInfo> _groupSummary;
1518

1619
Aggregator<T>[] _totalAggregators;
17-
string[] _totalSelectors;
18-
19-
string[] _groupSummaryTypes;
20-
string[] _groupSelectors;
2120
Stack<Aggregator<T>[]> _groupAggregatorsStack;
2221

2322

24-
public AggregateCalculator(IEnumerable data, IAccessor<T> accessor, IEnumerable<SummaryInfo> totalSummary, IEnumerable<SummaryInfo> groupSummary) {
23+
public AggregateCalculator(IEnumerable data, IAccessor<T> accessor, IList<SummaryInfo> totalSummary, IList<SummaryInfo> groupSummary, SumFix sumFix = null) {
2524
_data = data;
2625
_accessor = accessor;
26+
_totalSummary = totalSummary;
27+
_groupSummary = groupSummary;
28+
_sumFix = sumFix ?? new SumFix(typeof(T), totalSummary, groupSummary);
2729

28-
if(totalSummary != null) {
29-
_totalAggregators = totalSummary.Select(i => CreateAggregator(i.SummaryType)).ToArray();
30-
_totalSelectors = totalSummary.Select(i => i.Selector).ToArray();
31-
}
30+
_totalAggregators = _totalSummary?.Select(CreateAggregator).ToArray();
3231

33-
if(groupSummary != null) {
34-
_groupSummaryTypes = groupSummary.Select(i => i.SummaryType).ToArray();
35-
_groupSelectors = groupSummary.Select(i => i.Selector).ToArray();
32+
if(groupSummary != null)
3633
_groupAggregatorsStack = new Stack<Aggregator<T>[]>();
37-
}
3834
}
3935

4036
public object[] Run() {
4137
foreach(var item in _data)
4238
ProcessItem(item);
4339

44-
if(_totalAggregators != null)
45-
return Finish(_totalAggregators);
40+
if(_totalAggregators != null) {
41+
var values = Finish(_totalAggregators);
42+
_sumFix.ApplyToTotal(values);
43+
return values;
44+
}
4645

4746
return null;
4847
}
@@ -53,37 +52,41 @@ void ProcessItem(object item) {
5352
} else {
5453
if(_groupAggregatorsStack != null) {
5554
foreach(var groupAggregators in _groupAggregatorsStack)
56-
Step(item, groupAggregators, _groupSelectors);
55+
Step(item, groupAggregators, _groupSummary);
5756
}
5857

5958
if(_totalAggregators != null)
60-
Step(item, _totalAggregators, _totalSelectors);
59+
Step(item, _totalAggregators, _totalSummary);
6160
}
6261
}
6362

6463
void ProcessGroup(Group group) {
6564
if(_groupAggregatorsStack != null)
66-
_groupAggregatorsStack.Push(_groupSummaryTypes.Select(CreateAggregator).ToArray());
65+
_groupAggregatorsStack.Push(_groupSummary.Select(CreateAggregator).ToArray());
6766

6867
foreach(var i in group.items)
6968
ProcessItem(i);
7069

71-
if(_groupAggregatorsStack != null)
70+
if(_groupAggregatorsStack != null) {
7271
group.summary = Finish(_groupAggregatorsStack.Pop());
72+
_sumFix.ApplyToGroup(group.summary);
73+
}
7374
}
7475

75-
void Step(object obj, Aggregator<T>[] aggregators, string[] selectors) {
76+
void Step(object obj, Aggregator<T>[] aggregators, IList<SummaryInfo> summary) {
7677
var typed = (T)obj;
7778
for(var i = 0; i < aggregators.Length; i++)
78-
aggregators[i].Step(typed, selectors[i]);
79+
aggregators[i].Step(typed, summary[i].Selector);
7980
}
8081

8182
object[] Finish(Aggregator<T>[] aggregators) {
8283
return aggregators.Select(a => a.Finish()).ToArray();
8384
}
8485

8586

86-
Aggregator<T> CreateAggregator(string summaryType) {
87+
Aggregator<T> CreateAggregator(SummaryInfo summaryInfo) {
88+
var summaryType = summaryInfo.SummaryType;
89+
8790
switch(summaryType) {
8891
case AggregateName.SUM:
8992
return new SumAggregator<T>(_accessor);
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using DevExtreme.AspNet.Data.Aggregation.Accumulators;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Linq.Expressions;
6+
7+
namespace DevExtreme.AspNet.Data.Aggregation {
8+
9+
// Rationale: normalization across LINQ providers
10+
// https://github.com/aspnet/EntityFrameworkCore/issues/12307
11+
// https://data.uservoice.com/forums/72025/suggestions/2410716
12+
// https://dba.stackexchange.com/q/25435
13+
// https://en.wikipedia.org/wiki/Empty_sum
14+
15+
class SumFix : ExpressionCompiler {
16+
Expression _typeParam;
17+
IList<SummaryInfo> _totalSummary;
18+
IList<SummaryInfo> _groupSummary;
19+
IDictionary<string, object> _defaultValues;
20+
21+
public SumFix(Type type, IList<SummaryInfo> totalSummary, IList<SummaryInfo> groupSummary)
22+
: base(false) {
23+
_typeParam = Expression.Parameter(type);
24+
_totalSummary = totalSummary;
25+
_groupSummary = groupSummary;
26+
}
27+
28+
public void ApplyToTotal(object[] values) {
29+
Apply(_totalSummary, values);
30+
}
31+
32+
public void ApplyToGroup(object[] values) {
33+
Apply(_groupSummary, values);
34+
}
35+
36+
void Apply(IList<SummaryInfo> summary, object[] values) {
37+
if(summary == null)
38+
return;
39+
40+
for(var i = 0; i < summary.Count; i++) {
41+
if(values[i] != null)
42+
continue;
43+
44+
var summaryItem = summary[i];
45+
if(summaryItem.SummaryType != AggregateName.SUM)
46+
continue;
47+
48+
values[i] = GetDefaultValue(summaryItem.Selector);
49+
}
50+
}
51+
52+
object GetDefaultValue(string selector) {
53+
if(_defaultValues == null)
54+
_defaultValues = new Dictionary<string, object>();
55+
56+
if(!_defaultValues.ContainsKey(selector)) {
57+
var expr = CompileAccessorExpression(_typeParam, selector);
58+
var acc = AccumulatorFactory.Create(Utils.StripNullableType(expr.Type));
59+
_defaultValues[selector] = acc.GetValue();
60+
}
61+
62+
return _defaultValues[selector];
63+
}
64+
}
65+
66+
}

net/DevExtreme.AspNet.Data/DataSourceLoaderImpl.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ int ExecCount() {
147147

148148
RemoteGroupingResult ExecRemoteGrouping() {
149149
return RemoteGroupTransformer.Run(
150+
typeof(S),
150151
ExecExpr<AnonType>(Source, Builder.BuildLoadGroupsExpr(Source.Expression)),
151152
Options.HasGroups ? Options.Group.Length : 0,
152153
Options.TotalSummary,

net/DevExtreme.AspNet.Data/RemoteGrouping/RemoteGroupTransformer.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace DevExtreme.AspNet.Data.RemoteGrouping {
1111

1212
class RemoteGroupTransformer {
1313

14-
public static RemoteGroupingResult Run(IEnumerable<AnonType> flatGroups, int groupCount, SummaryInfo[] totalSummary, SummaryInfo[] groupSummary) {
14+
public static RemoteGroupingResult Run(Type sourceItemType, IEnumerable<AnonType> flatGroups, int groupCount, SummaryInfo[] totalSummary, SummaryInfo[] groupSummary) {
1515
List<Group> hierGroups = null;
1616

1717
if(groupCount > 0) {
@@ -31,7 +31,8 @@ public static RemoteGroupingResult Run(IEnumerable<AnonType> flatGroups, int gro
3131

3232
transformedTotalSummary.Add(new SummaryInfo { SummaryType = AggregateName.REMOTE_COUNT });
3333

34-
var totals = new AggregateCalculator<AnonType>(dataToAggregate, AnonTypeAccessor.Instance, transformedTotalSummary, transformedGroupSummary).Run();
34+
var sumFix = new SumFix(sourceItemType, totalSummary, groupSummary);
35+
var totals = new AggregateCalculator<AnonType>(dataToAggregate, AnonTypeAccessor.Instance, transformedTotalSummary, transformedGroupSummary, sumFix).Run();
3536
var totalCount = (int)totals.Last();
3637

3738
totals = totals.Take(totals.Length - 1).ToArray();

0 commit comments

Comments
 (0)