Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(common): interval multiplication and division #8620

Merged
merged 4 commits into from
Mar 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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