1+ // Copyright (C) 2025 Xtensive LLC.
2+ // This code is distributed under MIT license terms.
3+ // See the License.txt file in the project root for more information.
4+
5+ using System ;
6+ using System . Collections . Generic ;
7+ using System . Linq ;
8+ using Xtensive . Orm . Model ;
9+ using Xtensive . Orm . Rse . Providers ;
10+ using Xtensive . Reflection ;
11+
12+
13+ namespace Xtensive . Orm . Rse . Transformation
14+ {
15+ internal sealed class DecimalAggregateColumnRewriter : CompilableProviderVisitor
16+ {
17+ private readonly List < CalculateProvider > calculateProviders = new ( ) ;
18+ private readonly DomainModel domainModel ;
19+ private readonly CompilableProvider rootProvider ;
20+
21+ public CompilableProvider Rewrite ( )
22+ {
23+ return VisitCompilable ( rootProvider ) ;
24+ }
25+
26+ protected override Provider VisitAggregate ( AggregateProvider provider )
27+ {
28+ OnRecursionEntrance ( provider ) ;
29+ var source = VisitCompilable ( provider . Source ) ;
30+ var resultParameters = OnRecursionExit ( provider ) ;
31+ var shouldUseNewProvider = source != provider . Source && resultParameters == null ;
32+
33+ var aggregateColumns = provider . AggregateColumns ;
34+ var headerColumns = source . Header . Columns ;
35+ var newDescriptors = new AggregateColumnDescriptor [ aggregateColumns . Length ] ;
36+
37+ for ( int i = 0 , count = aggregateColumns . Length ; i < count ; i ++ ) {
38+ var column = aggregateColumns [ i ] ;
39+ if ( column . Type == WellKnownTypes . Decimal ) {
40+ var originDescriptor = column . Descriptor ;
41+ var aggregatedColumn = headerColumns [ originDescriptor . SourceIndex ] ;
42+
43+ var hints = TryGuessDecimalPrecisionAndSclale ( aggregatedColumn , source ) ;
44+ if ( hints . HasValue ) {
45+ newDescriptors [ i ] = new AggregateColumnDescriptor ( originDescriptor . Name , originDescriptor . SourceIndex , originDescriptor . AggregateType , hints . Value ) ;
46+ shouldUseNewProvider = true ;
47+ continue ;
48+ }
49+ }
50+ newDescriptors [ i ] = column . Descriptor ;
51+ }
52+
53+ if ( ! shouldUseNewProvider ) {
54+ return provider ;
55+ }
56+
57+ return source . Aggregate ( provider . GroupColumnIndexes , newDescriptors ) ;
58+ }
59+
60+ protected override Provider VisitCalculate ( CalculateProvider provider )
61+ {
62+ var visitedProvider = base . VisitCalculate ( provider ) ;
63+ calculateProviders . Add ( ( CalculateProvider ) visitedProvider ) ;
64+ return visitedProvider ;
65+ }
66+
67+
68+ private ( int , int ) ? TryGuessDecimalPrecisionAndSclale (
69+ Column aggregatedColumn , CompilableProvider originDataSource )
70+ {
71+ var headerColumns = originDataSource . Header . Columns ;
72+
73+ if ( aggregatedColumn is MappedColumn mColumn ) {
74+ var resolvedColumn = mColumn . ColumnInfoRef . Resolve ( domainModel ) ;
75+ if ( resolvedColumn . Precision . HasValue && resolvedColumn . Scale . HasValue )
76+ return ( resolvedColumn . Precision . Value , resolvedColumn . Scale . Value ) ;
77+ }
78+ else if ( aggregatedColumn is CalculatedColumn cColumn ) {
79+ if ( headerColumns . Count == 1 ) {
80+ // If current source contains only calculated column which is aggregate,
81+ // that means it uses indexes of its source in the calculated column
82+ var ownerProvider = calculateProviders . FirstOrDefault ( cp => cp . CalculatedColumns . Contains ( cColumn ) ) ;
83+ if ( ownerProvider == null )
84+ return null ;
85+ headerColumns = ownerProvider . Header . Columns ;
86+ }
87+ var expression = cColumn . Expression ;
88+ var usedColumns = new TupleAccessGatherer ( ) . Gather ( expression ) ;
89+
90+ var maxFloorDigits = - 1 ;
91+ var maxScaleDigits = - 1 ;
92+ foreach ( var cIndex in usedColumns . Distinct ( ) ) {
93+ var usedColumn = headerColumns [ cIndex ] ;
94+ if ( usedColumn is MappedColumn mmColumn ) {
95+ var resolvedColumn = mmColumn . ColumnInfoRef . Resolve ( domainModel ) ;
96+
97+ ( int ? p , int ? s ) @params = Type . GetTypeCode ( resolvedColumn . ValueType ) switch {
98+ TypeCode . Decimal => ( resolvedColumn . Precision , resolvedColumn . Scale ) ,
99+ TypeCode . Int32 or TypeCode . UInt32 => ( 19 , 8 ) ,
100+ TypeCode . Int64 or TypeCode . UInt64 => ( 28 , 8 ) ,
101+ TypeCode . Byte or TypeCode . SByte => ( 8 , 5 ) ,
102+ TypeCode . Int16 or TypeCode . UInt16 => ( 10 , 5 ) ,
103+ _ => ( null , null ) ,
104+ } ;
105+
106+ if ( @params . p . HasValue && @params . s . HasValue ) {
107+ if ( maxScaleDigits < @params . s . Value )
108+ maxScaleDigits = @params . s . Value ;
109+ var floorDigits = @params . p . Value - @params . s . Value ;
110+ if ( maxFloorDigits < floorDigits )
111+ maxFloorDigits = floorDigits ;
112+ }
113+ }
114+ }
115+
116+ if ( maxFloorDigits == - 1 || maxScaleDigits == - 1 )
117+ return null ;
118+ if ( maxFloorDigits + maxScaleDigits <= 28 )
119+ return ( maxFloorDigits + maxScaleDigits , maxScaleDigits ) ;
120+ }
121+
122+ return null ;
123+ }
124+
125+ public DecimalAggregateColumnRewriter ( DomainModel model , CompilableProvider rootProvider )
126+ {
127+ domainModel = model ;
128+ this . rootProvider = rootProvider ;
129+ }
130+ }
131+ }
0 commit comments