Skip to content

Commit 537dcdc

Browse files
committed
Add support for ±Infinity and NaN in NUMBER
Extend `NUMBER` type to allow representing positive/negative infinity and not-a-number values. This will allow e.g. the following: - safely convert all values of `real` and `double` to `number` - read PostgreSQL's `decimal` values (PostgreSQL `decimal` can hold infinity, and the unconstrained decimal (with dynamic scale) can also hold not-a-number)
1 parent 6476886 commit 537dcdc

File tree

24 files changed

+1608
-140
lines changed

24 files changed

+1608
-140
lines changed

client/trino-client/src/main/java/io/trino/client/JsonDecodingUtils.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,22 @@ private static class NumberDecoder
272272
public Object decode(JsonParser parser)
273273
throws IOException
274274
{
275-
return new BigDecimal(parser.getValueAsString());
275+
String value = parser.getValueAsString();
276+
// There is no JDK builtin numeric class that can represent all NUMBER values losslessly.
277+
// Return finite values as BigDecimal and non-finite values as Double.
278+
// Alternative approach could be to introduce a Trino-specific type to hold these values,
279+
// but that would not be friendly for JDBC-based apps, which often are compiled against
280+
// just the JDBC interfaces.
281+
switch (value) {
282+
case "NaN":
283+
return Double.NaN;
284+
case "+Infinity":
285+
return Double.POSITIVE_INFINITY;
286+
case "-Infinity":
287+
return Double.NEGATIVE_INFINITY;
288+
default:
289+
return new BigDecimal(value);
290+
}
276291
}
277292
}
278293

core/trino-main/src/main/java/io/trino/operator/JoinDomainBuilder.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import static io.trino.spi.function.InvocationConvention.simpleConvention;
4444
import static io.trino.spi.predicate.Range.range;
4545
import static io.trino.spi.type.DoubleType.DOUBLE;
46+
import static io.trino.spi.type.NumberType.NUMBER;
4647
import static io.trino.spi.type.RealType.REAL;
4748
import static io.trino.spi.type.TypeUtils.isFloatingPointNaN;
4849
import static io.trino.spi.type.TypeUtils.readNativeValue;
@@ -113,8 +114,8 @@ public JoinDomainBuilder(
113114
this.maxFilterSizeInBytes = maxFilterSize.toBytes();
114115
this.notifyStateChange = requireNonNull(notifyStateChange, "notifyStateChange is null");
115116

116-
// Skipping DOUBLE and REAL in collectMinMaxValues to avoid dealing with NaN values
117-
this.collectMinMax = minMaxEnabled && type.isOrderable() && type != DOUBLE && type != REAL;
117+
// Skipping REAL, DOUBLE and NUMBER in collectMinMaxValues to avoid dealing with NaN values
118+
this.collectMinMax = minMaxEnabled && type.isOrderable() && type != REAL && type != DOUBLE && type != NUMBER;
118119

119120
MethodHandle readOperator = typeOperators.getReadValueOperator(type, simpleConvention(NULLABLE_RETURN, FLAT));
120121
readOperator = readOperator.asType(readOperator.type().changeReturnType(Object.class));

core/trino-main/src/main/java/io/trino/sql/planner/DomainTranslator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ protected ExtractionResult visitLogical(Logical node, Boolean complement)
400400
remainingExpression = residuals.get(0);
401401
}
402402
else if (matchingSingleSymbolDomains) {
403-
// Types REAL and DOUBLE require special handling because they include NaN value. In this case, we cannot rely on the union of domains.
403+
// Types REAL, DOUBLE and NUMBER require special handling because they include NaN value. In this case, we cannot rely on the union of domains.
404404
// That is because domains covering the value set partially might union up to a domain covering the whole value set.
405405
// While the component domains didn't include NaN, the resulting domain could be further translated to predicate "TRUE" or "a IS NOT NULL",
406406
// which is satisfied by NaN. So during domain union, NaN might be implicitly added.

core/trino-main/src/main/java/io/trino/testing/MaterializedResult.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import io.trino.spi.type.SqlTimeWithTimeZone;
3333
import io.trino.spi.type.SqlTimestamp;
3434
import io.trino.spi.type.SqlTimestampWithTimeZone;
35+
import io.trino.spi.type.TrinoNumber;
3536
import io.trino.spi.type.Type;
3637

3738
import java.math.BigDecimal;
@@ -324,7 +325,11 @@ private static MaterializedRow convertToTestTypes(MaterializedRow trinoRow)
324325
case SqlTimestamp sqlTimestamp -> sqlTimestamp.toLocalDateTime();
325326
case SqlTimestampWithTimeZone sqlTimestampWithTimeZone -> sqlTimestampWithTimeZone.toZonedDateTime();
326327
case SqlDecimal sqlDecimal -> sqlDecimal.toBigDecimal();
327-
case SqlNumber sqlNumber -> new BigDecimal(sqlNumber.stringified());
328+
case SqlNumber number -> switch (number.value()) {
329+
case TrinoNumber.NotANumber() -> Double.NaN;
330+
case TrinoNumber.Infinity(boolean negative) -> negative ? Double.NEGATIVE_INFINITY : Double.POSITIVE_INFINITY;
331+
case TrinoNumber.BigDecimalValue(BigDecimal bigDecimal) -> bigDecimal;
332+
};
328333
default -> trinoValue;
329334
};
330335
convertedValues.add(convertedValue);

core/trino-main/src/main/java/io/trino/type/DecimalCasts.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939

4040
import java.io.IOException;
4141
import java.math.BigDecimal;
42+
import java.util.Optional;
4243

4344
import static io.airlift.slice.Slices.utf8Slice;
4445
import static io.trino.spi.StandardErrorCode.INVALID_CAST_ARGUMENT;
@@ -498,7 +499,8 @@ public static TrinoNumber longDecimalToNumber(Int128 decimal, long precision, lo
498499
@UsedByGeneratedCode
499500
public static long numberToShortDecimal(TrinoNumber value, long precision, long scale, long tenToScale)
500501
{
501-
BigDecimal bigDecimal = value.toBigDecimal();
502+
BigDecimal bigDecimal = numberToBigDecimal(value)
503+
.orElseThrow(() -> new TrinoException(INVALID_CAST_ARGUMENT, format("Cannot cast NUMBER '%s' to DECIMAL(%s, %s)", value, precision, scale)));
502504
BigDecimal result;
503505
try {
504506
result = bigDecimal.setScale(DecimalConversions.intScale(scale), HALF_UP);
@@ -517,7 +519,8 @@ public static long numberToShortDecimal(TrinoNumber value, long precision, long
517519
@UsedByGeneratedCode
518520
public static Int128 numberToLongDecimal(TrinoNumber value, long precision, long scale, Int128 tenToScale)
519521
{
520-
BigDecimal bigDecimal = value.toBigDecimal();
522+
BigDecimal bigDecimal = numberToBigDecimal(value)
523+
.orElseThrow(() -> new TrinoException(INVALID_CAST_ARGUMENT, format("Cannot cast NUMBER '%s' to DECIMAL(%s, %s)", value, precision, scale)));
521524
BigDecimal result;
522525
try {
523526
result = bigDecimal.setScale(DecimalConversions.intScale(scale), HALF_UP);
@@ -533,6 +536,14 @@ public static Int128 numberToLongDecimal(TrinoNumber value, long precision, long
533536
return Int128.valueOf(result.unscaledValue());
534537
}
535538

539+
private static Optional<BigDecimal> numberToBigDecimal(TrinoNumber value)
540+
{
541+
return switch (value.toBigDecimal()) {
542+
case TrinoNumber.NotANumber _, TrinoNumber.Infinity _ -> Optional.empty();
543+
case TrinoNumber.BigDecimalValue(BigDecimal bigDecimal) -> Optional.of(bigDecimal);
544+
};
545+
}
546+
536547
@UsedByGeneratedCode
537548
public static Slice shortDecimalToVarchar(long decimal, long scale, long varcharLength)
538549
{

core/trino-main/src/main/java/io/trino/type/DoubleOperators.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
import io.trino.spi.function.ScalarOperator;
2525
import io.trino.spi.function.SqlType;
2626
import io.trino.spi.type.StandardTypes;
27+
import io.trino.spi.type.TrinoNumber;
2728

29+
import java.math.BigDecimal;
2830
import java.text.DecimalFormat;
2931
import java.text.DecimalFormatSymbols;
3032

@@ -174,6 +176,19 @@ public static long castToReal(@SqlType(StandardTypes.DOUBLE) double value)
174176
return floatToRawIntBits((float) value);
175177
}
176178

179+
@ScalarOperator(CAST)
180+
@SqlType(StandardTypes.NUMBER)
181+
public static TrinoNumber castToNumber(@SqlType(StandardTypes.DOUBLE) double value)
182+
{
183+
if (Double.isNaN(value)) {
184+
return TrinoNumber.from(new TrinoNumber.NotANumber());
185+
}
186+
if (Double.isInfinite(value)) {
187+
return TrinoNumber.from(new TrinoNumber.Infinity(value < 0.0));
188+
}
189+
return TrinoNumber.from(BigDecimal.valueOf(value));
190+
}
191+
177192
@ScalarOperator(CAST)
178193
@LiteralParameters("x")
179194
@SqlType("varchar(x)")

0 commit comments

Comments
 (0)