From 6ee7613feb7984938f0b64220c66f4d1c456a93b Mon Sep 17 00:00:00 2001 From: tranvantinhnbk Date: Tue, 13 Oct 2026 19:04:50 +0700 Subject: [PATCH v2] Fix interval input at minimum signed field values Apply the effective time-field sign before checked accumulation so that INT64_MIN microseconds can be read from interval output. Apply AGO while accumulating numeric fields to handle verbose output at the minimum day and month boundaries as well. Preserve cancellation between fields near the limits and reject values whose accumulated representation is out of range. Add regression coverage for exact output/input round trips, boundary errors, and existing AGO cancellation behavior. Bug: #19746 Reported-by: Ke Discussion: https://postgr.es/m/19746-cf1e9c2c7f390d44@postgresql.org --- src/backend/utils/adt/datetime.c | 93 ++++++++++++--------- src/test/regress/expected/interval.out | 110 +++++++++++++++++++++++++ src/test/regress/sql/interval.sql | 59 +++++++++++++ 3 files changed, 225 insertions(+), 37 deletions(-) diff --git a/src/backend/utils/adt/datetime.c b/src/backend/utils/adt/datetime.c index 04ebc632178..4e44794757c 100644 --- a/src/backend/utils/adt/datetime.c +++ b/src/backend/utils/adt/datetime.c @@ -43,7 +43,7 @@ static int DecodeTimeCommon(char *str, int fmask, int range, int *tmask, struct pg_itm *itm); static int DecodeTime(char *str, int fmask, int range, int *tmask, struct pg_tm *tm, fsec_t *fsec); -static int DecodeTimeForInterval(char *str, int fmask, int range, +static int DecodeTimeForInterval(char *str, int fmask, int range, bool negative, int *tmask, struct pg_itm_in *itm_in); static const datetkn *datebsearch(const char *key, const datetkn *base, int nel); static int DecodeDate(char *str, int fmask, int *tmask, bool *is2digits, @@ -643,12 +643,14 @@ AdjustMicroseconds(int64 val, double fval, int64 scale, static bool AdjustDays(int64 val, int scale, struct pg_itm_in *itm_in) { - int days; + int64 days; - if (val < INT_MIN || val > INT_MAX) + if (pg_mul_s64_overflow(val, scale, &days) || + pg_add_s64_overflow(itm_in->tm_mday, days, &days) || + days < INT_MIN || days > INT_MAX) return false; - return !pg_mul_s32_overflow((int32) val, scale, &days) && - !pg_add_s32_overflow(itm_in->tm_mday, days, &itm_in->tm_mday); + itm_in->tm_mday = (int) days; + return true; } /* @@ -2766,7 +2768,7 @@ DecodeTime(char *str, int fmask, int range, * itm_in->tm_usec. */ static int -DecodeTimeForInterval(char *str, int fmask, int range, +DecodeTimeForInterval(char *str, int fmask, int range, bool negative, int *tmask, struct pg_itm_in *itm_in) { struct pg_itm itm; @@ -2777,6 +2779,19 @@ DecodeTimeForInterval(char *str, int fmask, int range, if (dterr) return dterr; + /* + * Apply the sign before accumulating, since the magnitude of PG_INT64_MIN + * cannot be represented as a positive int64. DecodeTimeCommon() has + * checked that all these fields are nonnegative. + */ + if (negative) + { + itm.tm_hour = -itm.tm_hour; + itm.tm_min = -itm.tm_min; + itm.tm_sec = -itm.tm_sec; + itm.tm_usec = -itm.tm_usec; + } + itm_in->tm_usec = itm.tm_usec; if (!int64_multiply_add(itm.tm_hour, USECS_PER_HOUR, &itm_in->tm_usec) || !int64_multiply_add(itm.tm_min, USECS_PER_MINUTE, &itm_in->tm_usec) || @@ -3514,6 +3529,7 @@ DecodeInterval(char **field, int *ftype, int nf, int range, bool force_negative = false; bool is_before = false; bool parsing_unit_val = false; + bool negative; char *cp; int fmask = 0, tmask, @@ -3564,13 +3580,14 @@ DecodeInterval(char **field, int *ftype, int nf, int range, switch (ftype[i]) { case DTK_TIME: + negative = force_negative; + if (is_before) + negative = !negative; dterr = DecodeTimeForInterval(field[i], fmask, range, + negative, &tmask, itm_in); if (dterr) return dterr; - if (force_negative && - itm_in->tm_usec > 0) - itm_in->tm_usec = -itm_in->tm_usec; type = DTK_DAY; parsing_unit_val = false; break; @@ -3583,6 +3600,9 @@ DecodeInterval(char **field, int *ftype, int nf, int range, * it as well. */ Assert(*field[i] == '-' || *field[i] == '+'); + negative = *field[i] == '-' || force_negative; + if (is_before) + negative = !negative; /* * Check for signed hh:mm or hh:mm:ss. If so, process exactly @@ -3590,20 +3610,9 @@ DecodeInterval(char **field, int *ftype, int nf, int range, */ if (strchr(field[i] + 1, ':') != NULL && DecodeTimeForInterval(field[i] + 1, fmask, range, + negative, &tmask, itm_in) == 0) { - if (*field[i] == '-') - { - /* flip the sign on time field */ - if (itm_in->tm_usec == PG_INT64_MIN) - return DTERR_FIELD_OVERFLOW; - itm_in->tm_usec = -itm_in->tm_usec; - } - - if (force_negative && - itm_in->tm_usec > 0) - itm_in->tm_usec = -itm_in->tm_usec; - /* * Set the next type to be a day, if units are not * specified. This handles the case of '1 +02:03' since we @@ -3707,10 +3716,35 @@ DecodeInterval(char **field, int *ftype, int nf, int range, fval = -fval; } + /* + * Apply "ago" before accumulating, so that minimum negative + * field values need not fit in their positive form first. + */ + if (is_before) + { + if (val == PG_INT64_MIN) + { + /* Handle this without negating val in DTK_MICROSEC. */ + if (type != DTK_MICROSEC) + return DTERR_FIELD_OVERFLOW; + } + else + val = -val; + fval = -fval; + } + switch (type) { case DTK_MICROSEC: - if (!AdjustMicroseconds(val, fval, 1, itm_in)) + if (is_before && val == PG_INT64_MIN) + { + /* A previous field may offset this magnitude. */ + if (pg_sub_s64_overflow(itm_in->tm_usec, val, + &itm_in->tm_usec) || + !AdjustFractMicroseconds(fval, 1, itm_in)) + return DTERR_FIELD_OVERFLOW; + } + else if (!AdjustMicroseconds(val, fval, 1, itm_in)) return DTERR_FIELD_OVERFLOW; tmask = DTK_M(MICROSECOND); break; @@ -3877,21 +3911,6 @@ DecodeInterval(char **field, int *ftype, int nf, int range, if (parsing_unit_val) return DTERR_BAD_FORMAT; - /* finally, AGO negates everything */ - if (is_before) - { - if (itm_in->tm_usec == PG_INT64_MIN || - itm_in->tm_mday == INT_MIN || - itm_in->tm_mon == INT_MIN || - itm_in->tm_year == INT_MIN) - return DTERR_FIELD_OVERFLOW; - - itm_in->tm_usec = -itm_in->tm_usec; - itm_in->tm_mday = -itm_in->tm_mday; - itm_in->tm_mon = -itm_in->tm_mon; - itm_in->tm_year = -itm_in->tm_year; - } - return 0; } diff --git a/src/test/regress/expected/interval.out b/src/test/regress/expected/interval.out index a16e3ccdb2e..abcfb4c30e5 100644 --- a/src/test/regress/expected/interval.out +++ b/src/test/regress/expected/interval.out @@ -2326,3 +2326,113 @@ SELECT INTERVAL '+infinity -infinity'; ERROR: invalid input syntax for type interval: "+infinity -infinity" LINE 1: SELECT INTERVAL '+infinity -infinity'; ^ +-- Minimum fields must survive output/input without overflowing their magnitude. +CREATE TEMP TABLE interval_roundtrip (i interval); +INSERT INTO interval_roundtrip VALUES + ('-9223372036854775808 microseconds'), + ('9223372036854775807 microseconds'), + ('-2147483648 days'), + ('-2147483648 months'), + ('-2147483648 months -2147483648 days -9223372036854775807 microseconds'), + ('2147483647 months -2147483648 days -9223372036854775808 microseconds'), + ('-1 month 2 days -03:04:05.678901'), + ('1 month -2 days 03:04:05.678901'); +-- Compare the binary representations, not just the normalized interval values. +SET intervalstyle = postgres; +SELECT bool_and(interval_send(i) = interval_send(i::text::interval)) AS roundtrip + FROM interval_roundtrip; + roundtrip +----------- + t +(1 row) + +SET intervalstyle = postgres_verbose; +SELECT bool_and(interval_send(i) = interval_send(i::text::interval)) AS roundtrip + FROM interval_roundtrip; + roundtrip +----------- + t +(1 row) + +SET intervalstyle = sql_standard; +SELECT bool_and(interval_send(i) = interval_send(i::text::interval)) AS roundtrip + FROM interval_roundtrip; + roundtrip +----------- + t +(1 row) + +SET intervalstyle = iso_8601; +SELECT bool_and(interval_send(i) = interval_send(i::text::interval)) AS roundtrip + FROM interval_roundtrip; + roundtrip +----------- + t +(1 row) + +SET intervalstyle = postgres; +DROP TABLE interval_roundtrip; +-- Preserve cancellation between fields when applying "ago" near the limits. +SELECT interval_send('-9223372036854775808 us 1 millisecond ago'::interval) = + interval_send('9223372036854774808 us'::interval) AS microsecond_cancellation; + microsecond_cancellation +-------------------------- + t +(1 row) + +SELECT interval_send('-2147483648 days 1 week ago'::interval) = + interval_send('2147483641 days'::interval) AS day_cancellation; + day_cancellation +------------------ + t +(1 row) + +SELECT interval_send('-2147483647 months 1 year ago'::interval) = + interval_send('2147483635 months'::interval) AS month_cancellation; + month_cancellation +-------------------- + t +(1 row) + +-- Applying the sign early must not allow values outside the signed range. +SELECT s, pg_input_is_valid(s, 'interval') AS valid +FROM (VALUES + ('-2562047788:00:54.775808'), + ('-2562047788:00:54.775809'), + ('2562047788:00:54.775807'), + ('2562047788:00:54.775808'), + ('2562047788:00:54.775808 ago'), + ('-2562047788:00:54.775808 ago'), + ('2147483648 days ago'), + ('2147483649 days ago'), + ('-2147483648 days ago'), + ('2147483648 months ago'), + ('2147483649 months ago'), + ('-2147483648 months ago')) AS v(s); + s | valid +------------------------------+------- + -2562047788:00:54.775808 | t + -2562047788:00:54.775809 | f + 2562047788:00:54.775807 | t + 2562047788:00:54.775808 | f + 2562047788:00:54.775808 ago | t + -2562047788:00:54.775808 ago | f + 2147483648 days ago | t + 2147483649 days ago | f + -2147483648 days ago | f + 2147483648 months ago | t + 2147483649 months ago | f + -2147483648 months ago | f +(12 rows) + +-- A leading SQL-standard sign applies to an unsigned time field too. +SET intervalstyle = sql_standard; +SELECT interval_send('-1 2562047788:00:54.775808'::interval) = + interval_send('-1 days -9223372036854775808 microseconds'::interval) + AS same_fields; + same_fields +------------- + t +(1 row) + +SET intervalstyle = postgres; diff --git a/src/test/regress/sql/interval.sql b/src/test/regress/sql/interval.sql index 43bc793925e..02b4d31e413 100644 --- a/src/test/regress/sql/interval.sql +++ b/src/test/regress/sql/interval.sql @@ -824,3 +824,62 @@ SELECT INTERVAL 'yesterday'; SELECT INTERVAL 'infinity years'; SELECT INTERVAL 'infinity ago'; SELECT INTERVAL '+infinity -infinity'; + +-- Minimum fields must survive output/input without overflowing their magnitude. +CREATE TEMP TABLE interval_roundtrip (i interval); +INSERT INTO interval_roundtrip VALUES + ('-9223372036854775808 microseconds'), + ('9223372036854775807 microseconds'), + ('-2147483648 days'), + ('-2147483648 months'), + ('-2147483648 months -2147483648 days -9223372036854775807 microseconds'), + ('2147483647 months -2147483648 days -9223372036854775808 microseconds'), + ('-1 month 2 days -03:04:05.678901'), + ('1 month -2 days 03:04:05.678901'); + +-- Compare the binary representations, not just the normalized interval values. +SET intervalstyle = postgres; +SELECT bool_and(interval_send(i) = interval_send(i::text::interval)) AS roundtrip + FROM interval_roundtrip; +SET intervalstyle = postgres_verbose; +SELECT bool_and(interval_send(i) = interval_send(i::text::interval)) AS roundtrip + FROM interval_roundtrip; +SET intervalstyle = sql_standard; +SELECT bool_and(interval_send(i) = interval_send(i::text::interval)) AS roundtrip + FROM interval_roundtrip; +SET intervalstyle = iso_8601; +SELECT bool_and(interval_send(i) = interval_send(i::text::interval)) AS roundtrip + FROM interval_roundtrip; +SET intervalstyle = postgres; +DROP TABLE interval_roundtrip; + +-- Preserve cancellation between fields when applying "ago" near the limits. +SELECT interval_send('-9223372036854775808 us 1 millisecond ago'::interval) = + interval_send('9223372036854774808 us'::interval) AS microsecond_cancellation; +SELECT interval_send('-2147483648 days 1 week ago'::interval) = + interval_send('2147483641 days'::interval) AS day_cancellation; +SELECT interval_send('-2147483647 months 1 year ago'::interval) = + interval_send('2147483635 months'::interval) AS month_cancellation; + +-- Applying the sign early must not allow values outside the signed range. +SELECT s, pg_input_is_valid(s, 'interval') AS valid +FROM (VALUES + ('-2562047788:00:54.775808'), + ('-2562047788:00:54.775809'), + ('2562047788:00:54.775807'), + ('2562047788:00:54.775808'), + ('2562047788:00:54.775808 ago'), + ('-2562047788:00:54.775808 ago'), + ('2147483648 days ago'), + ('2147483649 days ago'), + ('-2147483648 days ago'), + ('2147483648 months ago'), + ('2147483649 months ago'), + ('-2147483648 months ago')) AS v(s); + +-- A leading SQL-standard sign applies to an unsigned time field too. +SET intervalstyle = sql_standard; +SELECT interval_send('-1 2562047788:00:54.775808'::interval) = + interval_send('-1 days -9223372036854775808 microseconds'::interval) + AS same_fields; +SET intervalstyle = postgres; base-commit: 45fc51fcbf4534ea2f6f85e36f8155dbd5ba9851 -- 2.46.0