Skip to content

Commit aaf1361

Browse files
committed
fix #81; add new rule DAP244 for mixing aggregate and non-aggregate data
1 parent 039fb24 commit aaf1361

File tree

6 files changed

+99
-6
lines changed

6 files changed

+99
-6
lines changed

docs/rules/DAP244.md

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# DAP244
2+
3+
A `SELECT` expression cannot mix aggregate and non-aggregate results;
4+
5+
Fine:
6+
7+
``` sql
8+
select Name
9+
from SomeTable
10+
```
11+
12+
or
13+
14+
``` sql
15+
select COUNT(1)
16+
from SomeTable
17+
```
18+
19+
Invalid:
20+
21+
or
22+
23+
``` sql
24+
select Name, COUNT(1)
25+
from SomeTable
26+
```
27+
28+
Aggregate functions are [listed here](https://learn.microsoft.com/sql/t-sql/functions/aggregate-functions-transact-sql).

src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.Diagnostics.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,6 @@ public static readonly DiagnosticDescriptor
9595
InterpolatedStringSqlExpression = SqlWarning("DAP241", "Interpolated string usage", "Data values should not be interpolated into SQL string - use parameters instead"),
9696
ConcatenatedStringSqlExpression = SqlWarning("DAP242", "Concatenated string usage", "Data values should not be concatenated into SQL string - use parameters instead"),
9797
InvalidDatepartToken = SqlWarning("DAP243", "Valid datepart token expected", "Date functions require a recognized datepart argument"),
98-
OnSelectAggregateAndNonAggregate = SqlWarning("DAP244", "SELECT aggregate mismatch", "SELECT has mixture of aggregate and non-aggregate expressions");
98+
SelectAggregateMismatch = SqlWarning("DAP244", "SELECT aggregate mismatch", "SELECT has mixture of aggregate and non-aggregate expressions");
9999
}
100100
}

src/Dapper.AOT.Analyzers/Internal/DiagnosticTSqlProcessor.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ protected override void OnSelectFirstTopError(Location location)
140140
protected override void OnSelectSingleRowWithoutWhere(Location location)
141141
=> OnDiagnostic(DapperAnalyzer.Diagnostics.SelectSingleRowWithoutWhere, location);
142142
protected override void OnSelectAggregateAndNonAggregate(Location location)
143-
=> OnDiagnostic(DapperAnalyzer.Diagnostics.OnSelectAggregateAndNonAggregate, location);
143+
=> OnDiagnostic(DapperAnalyzer.Diagnostics.SelectAggregateMismatch, location);
144144
protected override void OnSelectSingleTopError(Location location)
145145
=> OnDiagnostic(DapperAnalyzer.Diagnostics.SelectSingleTopError, location);
146146

src/Dapper.AOT.Analyzers/SqlAnalysis/TSqlProcessor.cs

+32-4
Original file line numberDiff line numberDiff line change
@@ -760,7 +760,7 @@ private void ValidateDateArg(ScalarExpression value)
760760
{
761761
if (!(value is ColumnReferenceExpression col
762762
&& col.MultiPartIdentifier.Count == 1 && IsAnyCaseInsensitive(
763-
col.MultiPartIdentifier[0].Value, DateTokens )))
763+
col.MultiPartIdentifier[0].Value, DateTokens)))
764764
{
765765
parser.OnInvalidDatepartToken(value);
766766
}
@@ -873,17 +873,45 @@ enum AggregateFlags
873873
None = 0,
874874
HaveAggregate = 1 << 0,
875875
HaveNonAggregate = 1 << 1,
876-
Uncertain = 1 << 2,
876+
Uncertain = 1 << 2,
877877
}
878+
878879
private AggregateFlags IsAggregate(ScalarExpression expression)
879880
{
880881
// any use of an aggregate function contributes HaveAggregate
881882
// - there could be unary/binary operations on that aggregate function
882-
// - column references inside the aggregate expression or a sub-query are fine,
883+
// - column references etc inside an aggregate expression,
883884
// otherwise they contribute HaveNonAggregate
884-
return AggregateFlags.Uncertain;
885+
switch (expression)
886+
{
887+
case Literal:
888+
return AggregateFlags.None;
889+
case UnaryExpression ue:
890+
return IsAggregate(ue.Expression);
891+
case BinaryExpression be:
892+
return IsAggregate(be.FirstExpression)
893+
| IsAggregate(be.SecondExpression);
894+
case FunctionCall func when IsAggregateFunction(func.FunctionName.Value):
895+
return AggregateFlags.HaveAggregate; // don't need to look inside
896+
case ScalarSubquery sq:
897+
throw new NotSupportedException();
898+
case IdentityFunctionCall:
899+
case PrimaryExpression:
900+
return AggregateFlags.HaveNonAggregate;
901+
default:
902+
return AggregateFlags.Uncertain;
903+
}
904+
905+
static bool IsAggregateFunction(string name)
906+
=> IsAnyCaseInsensitive(name, AggregateFunctions);
885907
}
886908

909+
static readonly string[] AggregateFunctions = [
910+
"APPROX_COUNT_DISTINCT", "AVG","CHECKSUM_AGG", "COUNT", "COUNT_BIG",
911+
"GROUPING", "GROUPING_ID", "MAX", "MIN", "STDEV",
912+
"STDEVP", "STRING_AGG", "SUM", "VAR", "VARP",
913+
];
914+
887915
private bool EnforceTop(ScalarExpression expr)
888916
{
889917
if (IsInt32(expr, out var i) == TryEvaluateResult.SuccessConstant && i.HasValue)

test/Dapper.AOT.Test/Verifiers/DAP231.cs

+10
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,14 @@ public Task SelectSingleRowWithoutFrom() => SqlVerifyAsync("""
3737
select HasRecords = case when exists (select top 1 1 from MyTable) then 1 else 0 end
3838
""", SqlParseInputFlags.SingleRow);
3939

40+
[Fact] // false positive https://github.com/DapperLib/DapperAOT/issues/81
41+
public Task DoNotReportForAggregate() => SqlVerifyAsync("""
42+
SELECT MAX(SomeColumn) FROM MyTable
43+
""", SqlParseInputFlags.SingleRow);
44+
45+
[Fact]
46+
public Task DoNotReportForMultipleAggregatesAndExpressions() => SqlVerifyAsync("""
47+
SELECT -MAX(SomeColumn), COUNT(1) + 32 FROM MyTable
48+
""", SqlParseInputFlags.SingleRow);
49+
4050
}
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using Dapper.CodeAnalysis;
2+
using System.Threading.Tasks;
3+
using Xunit;
4+
using Diagnostics = Dapper.CodeAnalysis.DapperAnalyzer.Diagnostics;
5+
namespace Dapper.AOT.Test.Verifiers;
6+
7+
public class DAP244 : Verifier<DapperAnalyzer>
8+
{
9+
[Fact]
10+
public Task ValidNonAggregate() => SqlVerifyAsync("""
11+
select Name
12+
from SomeTable
13+
""");
14+
15+
[Fact]
16+
public Task ValidAggregate() => SqlVerifyAsync("""
17+
select COUNT(1), COUNT(DISTINCT Name)
18+
from SomeTable
19+
""");
20+
21+
[Fact]
22+
public Task InvalidMix() => SqlVerifyAsync("""
23+
{|#0:select COUNT(1), COUNT(DISTINCT Name), Name
24+
from SomeTable|}
25+
""", Diagnostic(Diagnostics.SelectAggregateMismatch).WithLocation(0));
26+
27+
}

0 commit comments

Comments
 (0)