@@ -238,6 +238,8 @@ protected virtual void OnSelectFirstTopError(Location location)
238
238
=> OnError ( $ "SELECT for First* should use TOP 1", location ) ;
239
239
protected virtual void OnSelectSingleRowWithoutWhere ( Location location )
240
240
=> OnError ( $ "SELECT for single row without WHERE or (TOP and ORDER BY)", location ) ;
241
+ protected virtual void OnSelectAggregateAndNonAggregate ( Location location )
242
+ => OnError ( $ "SELECT has mixture of aggregate and non-aggregate expressions", location ) ;
241
243
protected virtual void OnNonPositiveTop ( Location location )
242
244
=> OnError ( $ "TOP literals should be positive", location ) ;
243
245
protected virtual void OnNonPositiveFetch ( Location location )
@@ -250,6 +252,9 @@ protected virtual void OnFromMultiTableMissingAlias(Location location)
250
252
=> OnError ( $ "FROM expressions with multiple elements should use aliases", location ) ;
251
253
protected virtual void OnFromMultiTableUnqualifiedColumn ( Location location , string name )
252
254
=> OnError ( $ "FROM expressions with multiple elements should qualify all columns; it is unclear where '{ name } ' is located", location ) ;
255
+
256
+ protected virtual void OnInvalidDatepartToken ( Location location )
257
+ => OnError ( $ "Valid datepart token expected", location ) ;
253
258
protected virtual void OnTopWithOffset ( Location location )
254
259
=> OnError ( $ "TOP cannot be used when OFFSET is specified", location ) ;
255
260
@@ -702,6 +707,66 @@ public override void Visit(QuerySpecification node)
702
707
base . Visit ( node ) ;
703
708
}
704
709
710
+ public override void ExplicitVisit ( FunctionCall node )
711
+ {
712
+ ScalarExpression ? ignore = null ;
713
+ if ( node . Parameters . Count != 0 && IsSpecialDateFunction ( node . FunctionName . Value ) )
714
+ {
715
+ ValidateDateArg ( ignore = node . Parameters [ 0 ] ) ;
716
+ }
717
+ var oldIgnore = _demandAliases . IgnoreNode ; // stash
718
+ _demandAliases . IgnoreNode = ignore ;
719
+ base . ExplicitVisit ( node ) ; // dive
720
+ _demandAliases . IgnoreNode = oldIgnore ; // restore
721
+
722
+ static bool IsSpecialDateFunction ( string name )
723
+ => name . StartsWith ( "DATE" , StringComparison . OrdinalIgnoreCase )
724
+ && IsAnyCaseInsensitive ( name , DateTokenFunctions ) ;
725
+ }
726
+
727
+ static bool IsAnyCaseInsensitive ( string value , string [ ] options )
728
+ {
729
+ if ( value is not null )
730
+ {
731
+ foreach ( var option in options )
732
+ {
733
+ if ( string . Equals ( option , value , StringComparison . OrdinalIgnoreCase ) )
734
+ {
735
+ return true ;
736
+ }
737
+ }
738
+ }
739
+ return false ;
740
+ }
741
+ // arg0 has special meaning - not a column/etc
742
+ static readonly string [ ] DateTokenFunctions = [ "DATE_BUCKET" , "DATEADD" , "DATEDIFF" , "DATEDIFF_BIG" , "DATENAME" , "DATEPART" , "DATETRUNC" ] ;
743
+ static readonly string [ ] DateTokens = [
744
+ "year" , "yy" , "yyyy" ,
745
+ "quarter" , "qq" , "q" ,
746
+ "month" , "mm" , "m" ,
747
+ "dayofyear" , "dy" , "y" ,
748
+ "day" , "dd" , "d" ,
749
+ "week" , "wk" , "ww" ,
750
+ "weekday" , "dw" , "w" ,
751
+ "hour" , "hh" ,
752
+ "minute" , "mi" , "n" ,
753
+ "second" , "ss" , "s" ,
754
+ "millisecond" , "ms" ,
755
+ "microsecond" , "mcs" ,
756
+ "nanosecond" , "ns"
757
+ ] ;
758
+
759
+ private void ValidateDateArg ( ScalarExpression value )
760
+ {
761
+ if ( ! ( value is ColumnReferenceExpression col
762
+ && col . MultiPartIdentifier . Count == 1 && IsAnyCaseInsensitive (
763
+ col . MultiPartIdentifier [ 0 ] . Value , DateTokens ) ) )
764
+ {
765
+ parser . OnInvalidDatepartToken ( value ) ;
766
+ }
767
+
768
+ }
769
+
705
770
public override void Visit ( SelectStatement node )
706
771
{
707
772
if ( node . QueryExpression is QuerySpecification spec )
@@ -717,13 +782,15 @@ public override void Visit(SelectStatement node)
717
782
var checkNames = ValidateSelectNames ;
718
783
HashSet < string > names = checkNames ? new HashSet < string > ( StringComparer . InvariantCultureIgnoreCase ) : null ! ;
719
784
785
+ var aggregate = AggregateFlags . None ;
720
786
int index = 0 ;
721
787
foreach ( var el in spec . SelectElements )
722
788
{
723
789
switch ( el )
724
790
{
725
791
case SelectStarExpression :
726
792
parser . OnSelectStar ( el ) ;
793
+ aggregate |= AggregateFlags . HaveNonAggregate ;
727
794
reads ++ ;
728
795
break ;
729
796
case SelectScalarExpression scalar :
@@ -747,6 +814,7 @@ public override void Visit(SelectStatement node)
747
814
parser . OnSelectDuplicateColumnName ( scalar , name ! ) ;
748
815
}
749
816
}
817
+ aggregate |= IsAggregate ( scalar . Expression ) ;
750
818
reads ++ ;
751
819
break ;
752
820
case SelectSetVariable :
@@ -755,33 +823,43 @@ public override void Visit(SelectStatement node)
755
823
}
756
824
index ++ ;
757
825
}
826
+
827
+ if ( aggregate == ( AggregateFlags . HaveAggregate | AggregateFlags . HaveNonAggregate ) )
828
+ {
829
+ parser . OnSelectAggregateAndNonAggregate ( spec ) ;
830
+ }
831
+
758
832
if ( reads != 0 )
759
833
{
760
834
if ( sets != 0 )
761
835
{
762
836
parser . OnSelectAssignAndRead ( spec ) ;
763
837
}
764
- bool firstQuery = AddQuery ( ) ;
765
- if ( firstQuery && SingleRow // optionally enforce single-row validation
766
- && spec . FromClause is not null ) // no "from" is things like 'select @id, @name' - always one row
838
+ if ( node . Into is null ) // otherwise not actually a query
767
839
{
768
- bool haveTopOrFetch = false ;
769
- if ( spec . TopRowFilter is { Percent : false , Expression : ScalarExpression top } )
770
- {
771
- haveTopOrFetch = EnforceTop ( top ) ;
772
- }
773
- else if ( spec . OffsetClause is { FetchExpression : ScalarExpression fetch } )
840
+ bool firstQuery = AddQuery ( ) ;
841
+ if ( firstQuery && SingleRow // optionally enforce single-row validation
842
+ && spec . FromClause is not null ) // no "from" is things like 'select @id, @name' - always one row
774
843
{
775
- haveTopOrFetch = EnforceTop ( fetch ) ;
776
- }
844
+ bool haveTopOrFetch = false ;
845
+ if ( spec . TopRowFilter is { Percent : false , Expression : ScalarExpression top } )
846
+ {
847
+ haveTopOrFetch = EnforceTop ( top ) ;
848
+ }
849
+ else if ( spec . OffsetClause is { FetchExpression : ScalarExpression fetch } )
850
+ {
851
+ haveTopOrFetch = EnforceTop ( fetch ) ;
852
+ }
777
853
778
- // we want *either* a WHERE (which we will allow with/without a TOP),
779
- // or a TOP + ORDER BY
780
- if ( ! IsUnfiltered ( spec . FromClause , spec . WhereClause ) ) { } // fine
781
- else if ( haveTopOrFetch && spec . OrderByClause is not null ) { } // fine
782
- else
783
- {
784
- parser . OnSelectSingleRowWithoutWhere ( node ) ;
854
+ // we want *either* a WHERE (which we will allow with/without a TOP),
855
+ // or a TOP + ORDER BY
856
+ if ( ! IsUnfiltered ( spec . FromClause , spec . WhereClause ) ) { } // fine
857
+ else if ( haveTopOrFetch && spec . OrderByClause is not null ) { } // fine
858
+ else if ( ( aggregate & ( AggregateFlags . HaveAggregate | AggregateFlags . Uncertain ) ) != 0 ) { } // fine
859
+ else
860
+ {
861
+ parser . OnSelectSingleRowWithoutWhere ( node ) ;
862
+ }
785
863
}
786
864
}
787
865
}
@@ -790,6 +868,50 @@ public override void Visit(SelectStatement node)
790
868
base . Visit ( node ) ;
791
869
}
792
870
871
+ enum AggregateFlags
872
+ {
873
+ None = 0 ,
874
+ HaveAggregate = 1 << 0 ,
875
+ HaveNonAggregate = 1 << 1 ,
876
+ Uncertain = 1 << 2 ,
877
+ }
878
+
879
+ private AggregateFlags IsAggregate ( ScalarExpression expression )
880
+ {
881
+ // any use of an aggregate function contributes HaveAggregate
882
+ // - there could be unary/binary operations on that aggregate function
883
+ // - column references etc inside an aggregate expression,
884
+ // otherwise they contribute HaveNonAggregate
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 ) ;
907
+ }
908
+
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
+
793
915
private bool EnforceTop ( ScalarExpression expr )
794
916
{
795
917
if ( IsInt32 ( expr , out var i ) == TryEvaluateResult . SuccessConstant && i . HasValue )
@@ -1291,7 +1413,7 @@ bool TrySimplifyExpression(ScalarExpression node)
1291
1413
{
1292
1414
if ( _expressionAlreadyEvaluated ) return true ;
1293
1415
1294
- if ( node is UnaryExpression { UnaryExpressionType : UnaryExpressionType . Negative , Expression : IntegerLiteral or NumericLiteral } )
1416
+ if ( node is UnaryExpression { UnaryExpressionType : UnaryExpressionType . Negative , Expression : IntegerLiteral or NumericLiteral } )
1295
1417
{
1296
1418
// just "-4" or similar; don't warn the user to simplify that!
1297
1419
_expressionAlreadyEvaluated = true ;
@@ -1340,6 +1462,9 @@ public override void ExplicitVisit(BinaryExpression node)
1340
1462
1341
1463
public override void Visit ( OutputClause node )
1342
1464
{
1465
+ // note that this doesn't handle OUTPUT INTO,
1466
+ // which is via OutputIntoClause
1467
+
1343
1468
AddQuery ( ) ; // works like a query
1344
1469
base . Visit ( node ) ;
1345
1470
}
@@ -1409,6 +1534,8 @@ public DemandAliasesState(bool active, TableReference? amnesty)
1409
1534
public readonly bool Active ;
1410
1535
public readonly TableReference ? Amnesty ; // we can't validate the target until too late
1411
1536
public bool AmnestyNodeIsAlias ;
1537
+
1538
+ public ScalarExpression ? IgnoreNode { get ; set ; }
1412
1539
}
1413
1540
1414
1541
private DemandAliasesState _demandAliases ;
@@ -1435,7 +1562,8 @@ public override void Visit(TableReferenceWithAlias node)
1435
1562
}
1436
1563
public override void Visit ( ColumnReferenceExpression node )
1437
1564
{
1438
- if ( _demandAliases . Active && node . MultiPartIdentifier . Count == 1 )
1565
+ if ( _demandAliases . Active && node . MultiPartIdentifier . Count == 1
1566
+ && ! ReferenceEquals ( node , _demandAliases . IgnoreNode ) )
1439
1567
{
1440
1568
parser . OnFromMultiTableUnqualifiedColumn ( node , node . MultiPartIdentifier [ 0 ] . Value ) ;
1441
1569
}
0 commit comments