Description
ExpressionCreator.precisionTimestamp(boolean, LocalDateTime) builds a nanosecond precision_timestamp with arithmetic that does not report an out-of-range value, so a LocalDateTime outside what 64-bit nanoseconds can hold becomes a literal for a different instant:
|
public static Expression.PrecisionTimestampLiteral precisionTimestamp( |
|
boolean nullable, LocalDateTime value) { |
|
long epochNano = |
|
TimeUnit.SECONDS.toNanos(value.toEpochSecond(ZoneOffset.UTC)) + value.getNano(); |
|
return precisionTimestamp(nullable, epochNano, 9); |
|
} |
TimeUnit.SECONDS.toNanos saturates at Long.MIN_VALUE / Long.MAX_VALUE instead of throwing, and the + value.getNano() after it then wraps past the saturated value. A precision_timestamp<9> covers 1677-09-21 00:12:43.145224192 to 2262-04-11 23:47:16.854775807 (type_classes.md: an int64 count of 10^-P seconds since the epoch), while a LocalDateTime reaches years ±999999999, so any date outside that window is affected. The resulting literal round-trips through proto and isthmus unchanged, so nothing downstream notices.
The lowest instant in range is wrong as well, even though it fits: its floored seconds overflow on their own, the multiply saturates, and the sub-second part is then added on top of the saturated value.
Reproduction
long v = ExpressionCreator.precisionTimestamp(false, in).value();
// read back as Instant.ofEpochSecond(floorDiv(v, 1e9), floorMod(v, 1e9)) in UTC
input LocalDateTime |
value |
reads back as |
2262-04-11T23:47:16.854775807 |
9223372036854775807 |
2262-04-11T23:47:16.854775807 (correct) |
2262-04-11T23:47:16.854775808 |
-9223372036854775808 |
1677-09-21T00:12:43.145224192 |
2263-01-01T00:00 |
9223372036854775807 |
2262-04-11T23:47:16.854775807 |
9999-12-31T23:59:59.123456789 |
-9223372036731319020 |
1677-09-21T00:12:43.268680980 |
1600-01-01T00:00:00.123456789 |
-9223372036731319019 |
1677-09-21T00:12:43.268680981 |
1677-09-21T00:12:43.145224192 (fits: exactly Long.MIN_VALUE ns) |
-9223372036709551616 |
1677-09-21T00:12:43.290448384 |
Measured on main at 4bb02974.
Suggestion
Compute the count with Math.multiplyExact / Math.addExact and throw IllegalArgumentException naming the value when it does not fit, borrowing one second for a negative value with a fractional part so that the first second of the range still converts. #1318 does exactly this for isthmus' Calcite→Substrait timestamp literals (LiteralConverter.epochUnits), so the two entry points would agree on what they refuse. This is the only factory in ExpressionCreator built this way; precisionTime(boolean, LocalTime) is bounded by toNanoOfDay() and is safe.
🤖 Generated with AI
Description
ExpressionCreator.precisionTimestamp(boolean, LocalDateTime)builds a nanosecondprecision_timestampwith arithmetic that does not report an out-of-range value, so aLocalDateTimeoutside what 64-bit nanoseconds can hold becomes a literal for a different instant:substrait-java/core/src/main/java/io/substrait/expression/ExpressionCreator.java
Lines 228 to 233 in 4bb0297
TimeUnit.SECONDS.toNanossaturates atLong.MIN_VALUE/Long.MAX_VALUEinstead of throwing, and the+ value.getNano()after it then wraps past the saturated value. Aprecision_timestamp<9>covers 1677-09-21 00:12:43.145224192 to 2262-04-11 23:47:16.854775807 (type_classes.md: an int64 count of 10^-P seconds since the epoch), while aLocalDateTimereaches years ±999999999, so any date outside that window is affected. The resulting literal round-trips through proto and isthmus unchanged, so nothing downstream notices.The lowest instant in range is wrong as well, even though it fits: its floored seconds overflow on their own, the multiply saturates, and the sub-second part is then added on top of the saturated value.
Reproduction
LocalDateTimevalue2262-04-11T23:47:16.85477580792233720368547758072262-04-11T23:47:16.854775807(correct)2262-04-11T23:47:16.854775808-92233720368547758081677-09-21T00:12:43.1452241922263-01-01T00:0092233720368547758072262-04-11T23:47:16.8547758079999-12-31T23:59:59.123456789-92233720367313190201677-09-21T00:12:43.2686809801600-01-01T00:00:00.123456789-92233720367313190191677-09-21T00:12:43.2686809811677-09-21T00:12:43.145224192(fits: exactlyLong.MIN_VALUEns)-92233720367095516161677-09-21T00:12:43.290448384Measured on
mainat4bb02974.Suggestion
Compute the count with
Math.multiplyExact/Math.addExactand throwIllegalArgumentExceptionnaming the value when it does not fit, borrowing one second for a negative value with a fractional part so that the first second of the range still converts. #1318 does exactly this for isthmus' Calcite→Substrait timestamp literals (LiteralConverter.epochUnits), so the two entry points would agree on what they refuse. This is the only factory inExpressionCreatorbuilt this way;precisionTime(boolean, LocalTime)is bounded bytoNanoOfDay()and is safe.🤖 Generated with AI