Skip to content

Commit

Permalink
fix(common): interval multiplication and division (risingwavelabs#8620)
Browse files Browse the repository at this point in the history
  • Loading branch information
xiangjinwu authored Mar 18, 2023
1 parent 53261c5 commit 9a818b8
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 44 deletions.
15 changes: 15 additions & 0 deletions e2e_test/batch/types/interval.slt.part
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,18 @@ select '-2562047788:00:54.775808'::interval;

statement error
select '-2562047788:00:54.775809'::interval;

query T
select interval '3 mons -3 days' / 2;
----
1 mon 14 days -12:00:00

# The following is an overflow bug present in PostgreSQL 15.2
# Their `days` overflows to a negative value, leading to the latter smaller
# than the former. We report an error in this case.

statement ok
select interval '2147483647 mons 2147483647 days' * 0.999999991;

statement error out of range
select interval '2147483647 mons 2147483647 days' * 0.999999992;
4 changes: 2 additions & 2 deletions e2e_test/batch/types/temporal_arithmetic.slt.part
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ select real '0' * interval '1' second;
query T
select real '86' * interval '849884';
----
2 years 4 mons 5 days 22:47:04
20302:47:04

query T
select interval '1' second * real '6.1';
Expand All @@ -176,7 +176,7 @@ select interval '1' second * real '0';
query T
select interval '849884' * real '86';
----
2 years 4 mons 5 days 22:47:04
20302:47:04

query T
select '12:30:00'::time * 2;
Expand Down
98 changes: 67 additions & 31 deletions src/common/src/types/interval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,20 +81,6 @@ impl IntervalUnit {
self.usecs.rem_euclid(USECS_PER_DAY) as u64
}

#[deprecated]
fn from_total_usecs(usecs: i64) -> Self {
let mut remaining_usecs = usecs;
let months = remaining_usecs / USECS_PER_MONTH;
remaining_usecs -= months * USECS_PER_MONTH;
let days = remaining_usecs / USECS_PER_DAY;
remaining_usecs -= days * USECS_PER_DAY;
IntervalUnit {
months: (months as i32),
days: (days as i32),
usecs: remaining_usecs,
}
}

pub fn to_protobuf<T: Write>(self, output: &mut T) -> ArrayResult<usize> {
output.write_i32::<BigEndian>(self.months)?;
output.write_i32::<BigEndian>(self.days)?;
Expand All @@ -119,6 +105,63 @@ impl IntervalUnit {
})
}

/// Internal utility used by [`Self::mul_float`] and [`Self::div_float`] to adjust fractional
/// units. Not intended as general constructor.
fn from_floats(months: f64, days: f64, usecs: f64) -> Option<Self> {
// TSROUND in include/datatype/timestamp.h
// round eagerly at usecs precision because floats are imprecise
// should round to even #5576
let months_round_usecs =
|months: f64| (months * (USECS_PER_MONTH as f64)).round() / (USECS_PER_MONTH as f64);

let days_round_usecs =
|days: f64| (days * (USECS_PER_DAY as f64)).round() / (USECS_PER_DAY as f64);

let trunc_fract = |num: f64| (num.trunc(), num.fract());

// Handle months
let (months, months_fract) = trunc_fract(months_round_usecs(months));
if months.is_nan() || months < i32::MIN.into() || months > i32::MAX.into() {
return None;
}
let months = months as i32;
let (leftover_days, leftover_days_fract) =
trunc_fract(days_round_usecs(months_fract * 30.));

// Handle days
let (days, days_fract) = trunc_fract(days_round_usecs(days));
if days.is_nan() || days < i32::MIN.into() || days > i32::MAX.into() {
return None;
}
// Note that PostgreSQL split the integer part and fractional part individually before
// adding `leftover_days`. This makes a difference for mixed sign interval.
// For example in `interval '3 mons -3 days' / 2`
// * `leftover_days` is `15`
// * `days` from input is `-1.5`
// If we add first, we get `13.5` which is `13 days 12:00:00`;
// If we split first, we get `14` and `-0.5`, which ends up as `14 days -12:00:00`.
let (days_fract_whole, days_fract) =
trunc_fract(days_round_usecs(days_fract + leftover_days_fract));
let days = (days as i32)
.checked_add(leftover_days as i32)?
.checked_add(days_fract_whole as i32)?;
let leftover_usecs = days_fract * (USECS_PER_DAY as f64);

// Handle usecs
let result_usecs = usecs + leftover_usecs;
let usecs = result_usecs.round();
if usecs.is_nan() || usecs < (i64::MIN as f64) || usecs > (i64::MAX as f64) {
return None;
}
let usecs = usecs as i64;

Some(Self {
months,
days,
usecs,
})
}

/// Divides [`IntervalUnit`] by an integer/float with zero check.
pub fn div_float<I>(&self, rhs: I) -> Option<Self>
where
Expand All @@ -131,17 +174,11 @@ impl IntervalUnit {
return None;
}

#[expect(deprecated)]
let usecs = self.as_usecs_i64();
#[expect(deprecated)]
Some(IntervalUnit::from_total_usecs(
(usecs as f64 / rhs).round() as i64
))
}

#[deprecated]
fn as_usecs_i64(&self) -> i64 {
self.months as i64 * USECS_PER_MONTH + self.days as i64 * USECS_PER_DAY + self.usecs
Self::from_floats(
self.months as f64 / rhs,
self.days as f64 / rhs,
self.usecs as f64 / rhs,
)
}

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

#[expect(deprecated)]
let usecs = self.as_usecs_i64();
#[expect(deprecated)]
Some(IntervalUnit::from_total_usecs(
(usecs as f64 * rhs).round() as i64
))
Self::from_floats(
self.months as f64 * rhs,
self.days as f64 * rhs,
self.usecs as f64 * rhs,
)
}

/// Performs an exact division, returns [`None`] if for any unit, lhs % rhs != 0.
Expand Down
22 changes: 11 additions & 11 deletions src/tests/regress/data/sql/interval.sql
Original file line number Diff line number Diff line change
Expand Up @@ -111,17 +111,17 @@ INSERT INTO INTERVAL_MULDIV_TBL VALUES
('14 mon'),
('999 mon 999 days');

--@ SELECT span * 0.3 AS product
--@ FROM INTERVAL_MULDIV_TBL;
--@
--@ SELECT span * 8.2 AS product
--@ FROM INTERVAL_MULDIV_TBL;
--@
--@ SELECT span / 10 AS quotient
--@ FROM INTERVAL_MULDIV_TBL;
--@
--@ SELECT span / 100 AS quotient
--@ FROM INTERVAL_MULDIV_TBL;
SELECT span * 0.3 AS product
FROM INTERVAL_MULDIV_TBL;

SELECT span * 8.2 AS product
FROM INTERVAL_MULDIV_TBL;

SELECT span / 10 AS quotient
FROM INTERVAL_MULDIV_TBL;

SELECT span / 100 AS quotient
FROM INTERVAL_MULDIV_TBL;

DROP TABLE INTERVAL_MULDIV_TBL;

Expand Down

0 comments on commit 9a818b8

Please sign in to comment.