Skip to content

Commit 9a818b8

Browse files
authored
fix(common): interval multiplication and division (risingwavelabs#8620)
1 parent 53261c5 commit 9a818b8

File tree

4 files changed

+95
-44
lines changed

4 files changed

+95
-44
lines changed

e2e_test/batch/types/interval.slt.part

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,18 @@ select '-2562047788:00:54.775808'::interval;
150150

151151
statement error
152152
select '-2562047788:00:54.775809'::interval;
153+
154+
query T
155+
select interval '3 mons -3 days' / 2;
156+
----
157+
1 mon 14 days -12:00:00
158+
159+
# The following is an overflow bug present in PostgreSQL 15.2
160+
# Their `days` overflows to a negative value, leading to the latter smaller
161+
# than the former. We report an error in this case.
162+
163+
statement ok
164+
select interval '2147483647 mons 2147483647 days' * 0.999999991;
165+
166+
statement error out of range
167+
select interval '2147483647 mons 2147483647 days' * 0.999999992;

e2e_test/batch/types/temporal_arithmetic.slt.part

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ select real '0' * interval '1' second;
151151
query T
152152
select real '86' * interval '849884';
153153
----
154-
2 years 4 mons 5 days 22:47:04
154+
20302:47:04
155155

156156
query T
157157
select interval '1' second * real '6.1';
@@ -176,7 +176,7 @@ select interval '1' second * real '0';
176176
query T
177177
select interval '849884' * real '86';
178178
----
179-
2 years 4 mons 5 days 22:47:04
179+
20302:47:04
180180

181181
query T
182182
select '12:30:00'::time * 2;

src/common/src/types/interval.rs

Lines changed: 67 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -81,20 +81,6 @@ impl IntervalUnit {
8181
self.usecs.rem_euclid(USECS_PER_DAY) as u64
8282
}
8383

84-
#[deprecated]
85-
fn from_total_usecs(usecs: i64) -> Self {
86-
let mut remaining_usecs = usecs;
87-
let months = remaining_usecs / USECS_PER_MONTH;
88-
remaining_usecs -= months * USECS_PER_MONTH;
89-
let days = remaining_usecs / USECS_PER_DAY;
90-
remaining_usecs -= days * USECS_PER_DAY;
91-
IntervalUnit {
92-
months: (months as i32),
93-
days: (days as i32),
94-
usecs: remaining_usecs,
95-
}
96-
}
97-
9884
pub fn to_protobuf<T: Write>(self, output: &mut T) -> ArrayResult<usize> {
9985
output.write_i32::<BigEndian>(self.months)?;
10086
output.write_i32::<BigEndian>(self.days)?;
@@ -119,6 +105,63 @@ impl IntervalUnit {
119105
})
120106
}
121107

108+
/// Internal utility used by [`Self::mul_float`] and [`Self::div_float`] to adjust fractional
109+
/// units. Not intended as general constructor.
110+
fn from_floats(months: f64, days: f64, usecs: f64) -> Option<Self> {
111+
// TSROUND in include/datatype/timestamp.h
112+
// round eagerly at usecs precision because floats are imprecise
113+
// should round to even #5576
114+
let months_round_usecs =
115+
|months: f64| (months * (USECS_PER_MONTH as f64)).round() / (USECS_PER_MONTH as f64);
116+
117+
let days_round_usecs =
118+
|days: f64| (days * (USECS_PER_DAY as f64)).round() / (USECS_PER_DAY as f64);
119+
120+
let trunc_fract = |num: f64| (num.trunc(), num.fract());
121+
122+
// Handle months
123+
let (months, months_fract) = trunc_fract(months_round_usecs(months));
124+
if months.is_nan() || months < i32::MIN.into() || months > i32::MAX.into() {
125+
return None;
126+
}
127+
let months = months as i32;
128+
let (leftover_days, leftover_days_fract) =
129+
trunc_fract(days_round_usecs(months_fract * 30.));
130+
131+
// Handle days
132+
let (days, days_fract) = trunc_fract(days_round_usecs(days));
133+
if days.is_nan() || days < i32::MIN.into() || days > i32::MAX.into() {
134+
return None;
135+
}
136+
// Note that PostgreSQL split the integer part and fractional part individually before
137+
// adding `leftover_days`. This makes a difference for mixed sign interval.
138+
// For example in `interval '3 mons -3 days' / 2`
139+
// * `leftover_days` is `15`
140+
// * `days` from input is `-1.5`
141+
// If we add first, we get `13.5` which is `13 days 12:00:00`;
142+
// If we split first, we get `14` and `-0.5`, which ends up as `14 days -12:00:00`.
143+
let (days_fract_whole, days_fract) =
144+
trunc_fract(days_round_usecs(days_fract + leftover_days_fract));
145+
let days = (days as i32)
146+
.checked_add(leftover_days as i32)?
147+
.checked_add(days_fract_whole as i32)?;
148+
let leftover_usecs = days_fract * (USECS_PER_DAY as f64);
149+
150+
// Handle usecs
151+
let result_usecs = usecs + leftover_usecs;
152+
let usecs = result_usecs.round();
153+
if usecs.is_nan() || usecs < (i64::MIN as f64) || usecs > (i64::MAX as f64) {
154+
return None;
155+
}
156+
let usecs = usecs as i64;
157+
158+
Some(Self {
159+
months,
160+
days,
161+
usecs,
162+
})
163+
}
164+
122165
/// Divides [`IntervalUnit`] by an integer/float with zero check.
123166
pub fn div_float<I>(&self, rhs: I) -> Option<Self>
124167
where
@@ -131,17 +174,11 @@ impl IntervalUnit {
131174
return None;
132175
}
133176

134-
#[expect(deprecated)]
135-
let usecs = self.as_usecs_i64();
136-
#[expect(deprecated)]
137-
Some(IntervalUnit::from_total_usecs(
138-
(usecs as f64 / rhs).round() as i64
139-
))
140-
}
141-
142-
#[deprecated]
143-
fn as_usecs_i64(&self) -> i64 {
144-
self.months as i64 * USECS_PER_MONTH + self.days as i64 * USECS_PER_DAY + self.usecs
177+
Self::from_floats(
178+
self.months as f64 / rhs,
179+
self.days as f64 / rhs,
180+
self.usecs as f64 / rhs,
181+
)
145182
}
146183

147184
/// times [`IntervalUnit`] with an integer/float.
@@ -152,12 +189,11 @@ impl IntervalUnit {
152189
let rhs = rhs.try_into().ok()?;
153190
let rhs = rhs.0;
154191

155-
#[expect(deprecated)]
156-
let usecs = self.as_usecs_i64();
157-
#[expect(deprecated)]
158-
Some(IntervalUnit::from_total_usecs(
159-
(usecs as f64 * rhs).round() as i64
160-
))
192+
Self::from_floats(
193+
self.months as f64 * rhs,
194+
self.days as f64 * rhs,
195+
self.usecs as f64 * rhs,
196+
)
161197
}
162198

163199
/// Performs an exact division, returns [`None`] if for any unit, lhs % rhs != 0.

src/tests/regress/data/sql/interval.sql

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -111,17 +111,17 @@ INSERT INTO INTERVAL_MULDIV_TBL VALUES
111111
('14 mon'),
112112
('999 mon 999 days');
113113

114-
--@ SELECT span * 0.3 AS product
115-
--@ FROM INTERVAL_MULDIV_TBL;
116-
--@
117-
--@ SELECT span * 8.2 AS product
118-
--@ FROM INTERVAL_MULDIV_TBL;
119-
--@
120-
--@ SELECT span / 10 AS quotient
121-
--@ FROM INTERVAL_MULDIV_TBL;
122-
--@
123-
--@ SELECT span / 100 AS quotient
124-
--@ FROM INTERVAL_MULDIV_TBL;
114+
SELECT span * 0.3 AS product
115+
FROM INTERVAL_MULDIV_TBL;
116+
117+
SELECT span * 8.2 AS product
118+
FROM INTERVAL_MULDIV_TBL;
119+
120+
SELECT span / 10 AS quotient
121+
FROM INTERVAL_MULDIV_TBL;
122+
123+
SELECT span / 100 AS quotient
124+
FROM INTERVAL_MULDIV_TBL;
125125

126126
DROP TABLE INTERVAL_MULDIV_TBL;
127127

0 commit comments

Comments
 (0)