Skip to content

Commit e7fd245

Browse files
[fix](constant folding) Reject overflowing from_second and from_millisecond literals (#67037)
### What problem does this PR solve? Related PR: #28685 Problem Summary: `from_second` and `from_millisecond` widen their argument to microseconds **before** the range check runs, so the multiplication can wrap and land back inside the accepted range. `fromSecond` computes `second.getValue() * 1000 * 1000` and hands the result to `fromMicroSecond(long, int)`, whose guard is `microSecond < 0 || microSecond > 253402271999999999L`. The guard therefore only ever sees the wrapped product: | Expression | Product | Wraps to | Folds to | |-----------------------------------------|----------------------------------------|----------|-----------------------------------| | `from_second(18446744073710)` | `18446744073710 * 1000000` | `448384` | `1970-01-01 00:00:00.448384` | | `from_millisecond(18446744073709552)` | `18446744073709552 * 1000` | `384` | `1970-01-01 00:00:00.000384` | Both wrapped values sit inside `[0, 253402271999999999]`, so the guard passes. `from_microsecond` passes its argument through unmultiplied and is unaffected, which is why only the two widening wrappers are wrong. These functions are registered for FE constant folding, so this is the path a literal argument takes. The BE divides rather than multiplies (`from_unixtime(value / Impl::ratio, ...)`) and rejects the same value, so today the same expression errors over a column but folds to a bogus datetime over a literal. #28685 fixed the BE side of this family in 2023 and did not touch the FE fold path. ### What is changed and how does it work? The widening now goes through a small `toMicroSecond` helper that uses `Math.multiplyExact` and converts the resulting `ArithmeticException` into the same `AnalysisException` the range check already raises. An overflowing literal now reports "out of range" instead of folding. In-range arguments are unaffected, and negative arguments are still rejected by the existing guard — both covered by the new tests. ### Release note Fix `from_second` and `from_millisecond` returning a wrong datetime instead of an out-of-range error for a literal argument large enough to overflow when converted to microseconds.
1 parent 955f720 commit e7fd245

2 files changed

Lines changed: 65 additions & 2 deletions

File tree

fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/executable/DateTimeExtractAndTransform.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,12 +1092,14 @@ public static Expression monthName(DateV2Literal date) {
10921092

10931093
@ExecFunction(name = "from_second")
10941094
public static Expression fromSecond(BigIntLiteral second) {
1095-
return fromMicroSecond(second.getValue() * 1000 * 1000, FromSecond.RESULT_SCALE);
1095+
return fromMicroSecond(toMicroSecond(second.getValue(), 1000L * 1000L, "from_second"),
1096+
FromSecond.RESULT_SCALE);
10961097
}
10971098

10981099
@ExecFunction(name = "from_millisecond")
10991100
public static Expression fromMilliSecond(BigIntLiteral milliSecond) {
1100-
return fromMicroSecond(milliSecond.getValue() * 1000, FromMillisecond.RESULT_SCALE);
1101+
return fromMicroSecond(toMicroSecond(milliSecond.getValue(), 1000L, "from_millisecond"),
1102+
FromMillisecond.RESULT_SCALE);
11011103
}
11021104

11031105
@ExecFunction(name = "from_microsecond")
@@ -1117,6 +1119,23 @@ private static Expression fromMicroSecond(long microSecond, int scale) {
11171119
dateTime.getMinute(), dateTime.getSecond(), dateTime.getNano() / 1000);
11181120
}
11191121

1122+
/**
1123+
* Widens {@code value} to microseconds without letting the multiplication wrap.
1124+
*
1125+
* <p>The range check lives in {@link #fromMicroSecond(long, int)}, which runs after the
1126+
* widening, so a product that overflows long can land back inside the accepted range: a
1127+
* literal argument then folds to a bogus datetime instead of being reported as out of range.
1128+
* The BE divides rather than multiplies and rejects the same argument, so only constant
1129+
* folding on the FE was affected.
1130+
*/
1131+
private static long toMicroSecond(long value, long ratio, String functionName) {
1132+
try {
1133+
return Math.multiplyExact(value, ratio);
1134+
} catch (ArithmeticException e) {
1135+
throw new AnalysisException("Operation " + functionName + " of " + value + " out of range");
1136+
}
1137+
}
1138+
11201139
@ExecFunction(name = "microseconds_diff")
11211140
public static Expression microsecondsDiff(DateTimeV2Literal t1, DateTimeV2Literal t2) {
11221141
return new BigIntLiteral(DateTimeV2Literal.datetimeDiffInMicroSeconds(t1, t2));

fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/executable/DateTimeExtractAndTransformTest.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,4 +209,48 @@ void testConvertTzDstTransition() {
209209
new VarcharLiteral("Europe/Paris"),
210210
new VarcharLiteral("UTC")));
211211
}
212+
213+
// from_second and from_millisecond widen their argument to microseconds BEFORE the range
214+
// check in fromMicroSecond runs. Without an overflow-checked multiply the product wraps back
215+
// into the accepted range and folds to a bogus datetime instead of reporting the argument as
216+
// out of range: 18446744073710 * 1_000_000 wraps to 448384, and 18446744073709552 * 1000
217+
// wraps to 384. The BE rejects both, so only FE constant folding disagreed.
218+
@Test
219+
void testFromSecondRejectsOverflowingArgument() {
220+
Assertions.assertThrows(AnalysisException.class,
221+
() -> DateTimeExtractAndTransform.fromSecond(new BigIntLiteral(18446744073710L)));
222+
Assertions.assertThrows(AnalysisException.class,
223+
() -> DateTimeExtractAndTransform.fromSecond(new BigIntLiteral(Long.MAX_VALUE)));
224+
225+
Assertions.assertThrows(AnalysisException.class,
226+
() -> DateTimeExtractAndTransform.fromMilliSecond(new BigIntLiteral(18446744073709552L)));
227+
Assertions.assertThrows(AnalysisException.class,
228+
() -> DateTimeExtractAndTransform.fromMilliSecond(new BigIntLiteral(Long.MAX_VALUE)));
229+
}
230+
231+
// The pre-existing range check must keep rejecting a value that is out of range without
232+
// overflowing, and negative arguments must still be rejected.
233+
@Test
234+
void testFromSecondStillRejectsOutOfRangeArgument() {
235+
Assertions.assertThrows(AnalysisException.class,
236+
() -> DateTimeExtractAndTransform.fromSecond(new BigIntLiteral(253402272000L)));
237+
Assertions.assertThrows(AnalysisException.class,
238+
() -> DateTimeExtractAndTransform.fromSecond(new BigIntLiteral(-20L)));
239+
Assertions.assertThrows(AnalysisException.class,
240+
() -> DateTimeExtractAndTransform.fromMilliSecond(new BigIntLiteral(-20L)));
241+
}
242+
243+
// In-range arguments must still fold. The exact datetime depends on the session time zone,
244+
// so this only asserts that folding happens and produces a datetime literal.
245+
@Test
246+
void testFromSecondStillFoldsInRangeArgument() {
247+
Assertions.assertInstanceOf(DateTimeV2Literal.class,
248+
DateTimeExtractAndTransform.fromSecond(new BigIntLiteral(0L)));
249+
Assertions.assertInstanceOf(DateTimeV2Literal.class,
250+
DateTimeExtractAndTransform.fromSecond(new BigIntLiteral(1735689600L)));
251+
Assertions.assertInstanceOf(DateTimeV2Literal.class,
252+
DateTimeExtractAndTransform.fromMilliSecond(new BigIntLiteral(1735689600000L)));
253+
Assertions.assertInstanceOf(DateTimeV2Literal.class,
254+
DateTimeExtractAndTransform.fromMicroSecond(new BigIntLiteral(1735689600000000L)));
255+
}
212256
}

0 commit comments

Comments
 (0)