Skip to content

Commit 5791c83

Browse files
committed
Apply given decimal parameters hint to cast of aggregate expression
1 parent 3a6660c commit 5791c83

2 files changed

Lines changed: 62 additions & 5 deletions

File tree

Orm/Xtensive.Orm.PostgreSql/Orm.Providers.PostgreSql/SqlCompiler.cs

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ namespace Xtensive.Orm.Providers.PostgreSql
1919
{
2020
internal class SqlCompiler : Providers.SqlCompiler
2121
{
22-
private static readonly Type DecimalType = typeof(decimal);
23-
2422
protected override SqlProvider VisitFreeText(FreeTextProvider provider)
2523
{
2624
var rankColumnName = provider.Header.Columns[provider.Header.Columns.Count - 1].Name;
@@ -60,15 +58,73 @@ protected override SqlExpression ProcessAggregate(SqlProvider source, List<SqlEx
6058
var result = base.ProcessAggregate(source, sourceColumns, aggregateColumn);
6159
var aggregateType = aggregateColumn.AggregateType;
6260
var originCalculateColumn = source.Origin.Header.Columns[aggregateColumn.SourceIndex];
63-
if (aggregateType == AggregateType.Sum || aggregateType == AggregateType.Avg) {
64-
if (!IsCalculatedColumn(originCalculateColumn) && aggregateColumn.Type == DecimalType) {
65-
return result;
61+
if (AggregateRequiresDecimalAdjustments(aggregateColumn)) {
62+
if (!IsCalculatedColumn(originCalculateColumn)) {
63+
// this is aggregate by one column, result will be defined by the precision and scale of the column
64+
return result;
6665
}
66+
67+
// For expressions we had to try to guess result type parameters to avoid overflow exception
68+
// on reading something like 12.000000000000000000000000000000 (30 zeros) which in practice can be reduced
69+
// to 12.0 on reading but Npgsql does not allow us neither to turn on such conversion inside Npgsql (as it was in v3.x.x) nor
70+
// to get raw data and make conversion by ourselves (because nothing similar to SqlDecimal has provided by the library).
71+
72+
// Official answer of the Npgsql team is to either cast to DECIMAL with proper parameters or read all parameters as
73+
// strings and then convert :-)
74+
// Reading strings is not an option so we try to tell fortunes in a teacup :-(
75+
var resultType = (!TryAdjustPrecisionScale(aggregateColumn.Descriptor.DecimalParametersHint, aggregateType, out var newPrecision, out var newScale))
76+
? Driver.MapValueType(aggregateColumn.Type)
77+
: Driver.MapValueType(aggregateColumn.Type, null, newPrecision, newScale);
78+
return SqlDml.Cast(result, resultType);
79+
}
80+
else if (aggregateType != AggregateType.Count) {
6781
result = SqlDml.Cast(result, Driver.MapValueType(aggregateColumn.Type));
6882
}
6983
return result;
7084
}
7185

86+
private bool AggregateRequiresDecimalAdjustments(AggregateColumn aggregateColumn)
87+
{
88+
var aggregateType = aggregateColumn.AggregateType;
89+
return (aggregateType is AggregateType.Sum or AggregateType.Avg
90+
or AggregateType.Min or AggregateType.Min) && aggregateColumn.Type == WellKnownTypes.DecimalType;
91+
}
92+
93+
private bool TryAdjustPrecisionScale(
94+
in (int precision, int scale)? typeHint,
95+
in AggregateType aggregateType,
96+
out int precision, out int scale)
97+
{
98+
if (!typeHint.HasValue) {
99+
precision = -1;
100+
scale = -1;
101+
return false;
102+
}
103+
var typeHintValue = typeHint.Value;
104+
105+
if (typeHintValue.precision == 28) {
106+
// No room for adjust, otherwise we'll lose floor part data
107+
precision = typeHintValue.precision;
108+
scale = typeHintValue.scale;
109+
return true;
110+
}
111+
112+
// choose max available precision for .net or let it be the one user declared
113+
precision = (typeHintValue.precision < 28) ? 28 : typeHintValue.precision;
114+
115+
// It is benefitial to increase scale but for how much? It is open question,
116+
// sometimes we need bigger floor part, and sometimes bigger fractional part.
117+
// This algorithm is a trade-off.
118+
scale = aggregateType switch {
119+
AggregateType.Avg => (typeHintValue.precision < 28) ? typeHintValue.scale + Math.Max((precision - typeHintValue.precision) / 2, 1) : typeHintValue.scale + 1,
120+
AggregateType.Sum => (typeHintValue.precision < 27) ? typeHintValue.scale + 2 : typeHintValue.scale + 1,
121+
AggregateType.Min => (typeHintValue.precision < 27) ? typeHintValue.scale + 2 : typeHintValue.scale + 1,
122+
AggregateType.Max => (typeHintValue.precision < 27) ? typeHintValue.scale + 2 : typeHintValue.scale + 1,
123+
_ => typeHintValue.scale,
124+
};
125+
return true;
126+
}
127+
72128
private bool IsCalculatedColumn(Column column) => column is CalculatedColumn;
73129

74130
public SqlCompiler(HandlerAccessor handlers, CompilerConfiguration configuration)

Orm/Xtensive.Orm.PostgreSql/WellKnownTypes.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ internal static class WellKnownTypes
1616
public static readonly Type GuidType = typeof(Guid);
1717
public static readonly Type ByteArrayType = typeof(byte[]);
1818
public static readonly Type StringType = typeof(string);
19+
public static readonly Type DecimalType = typeof(decimal);
1920

2021
public static readonly Type NpgsqlPointType = typeof(NpgsqlPoint);
2122
public static readonly Type NpgsqlLSegType = typeof(NpgsqlLSeg);

0 commit comments

Comments
 (0)