From: Daniele Varrazzo Date: Wed, 8 Jun 2022 16:30:13 +0000 (+0200) Subject: fix: produce consistent error messages on date overflow X-Git-Tag: 3.1~62^2~3 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=e63406c65b94ca4c21d41b65787a74eea97ea234;p=thirdparty%2Fpsycopg.git fix: produce consistent error messages on date overflow - State if the date is too small or too large, not just "not supported". - Use similar messages in text and binary format. - Avoid an overflow error with the infinity date in Python. See #315 --- diff --git a/psycopg/psycopg/types/datetime.py b/psycopg/psycopg/types/datetime.py index b11a73522..ac9217aaa 100644 --- a/psycopg/psycopg/types/datetime.py +++ b/psycopg/psycopg/types/datetime.py @@ -259,11 +259,14 @@ class DateLoader(Loader): try: return date(int(ye), int(mo), int(da)) - except ValueError as e: + except ValueError as ex: s = bytes(data).decode("utf8", "replace") - if len(s) != 10: - raise DataError(f"date not supported: {s!r}") from None - raise DataError(f"can't parse date {s!r}: {e}") from None + if s == "infinity" or (s and len(s.split()[0]) > 10): + raise DataError(f"date too large (after year 10K): {s!r}") from None + elif s == "-infinity" or "BC" in s: + raise DataError(f"date too small (before year 1): {s!r}") from None + else: + raise DataError(f"can't parse date {s!r}: {ex}") from None class DateBinaryLoader(Loader): @@ -274,7 +277,7 @@ class DateBinaryLoader(Loader): days = unpack_int4(data)[0] + _pg_date_epoch_days try: return date.fromordinal(days) - except ValueError: + except (ValueError, OverflowError): if days < _py_date_min_days: raise DataError("date too small (before year 1)") from None else: diff --git a/psycopg_c/psycopg_c/types/datetime.pyx b/psycopg_c/psycopg_c/types/datetime.pyx index 6ffa26447..d84dc0699 100644 --- a/psycopg_c/psycopg_c/types/datetime.pyx +++ b/psycopg_c/psycopg_c/types/datetime.pyx @@ -365,10 +365,18 @@ cdef class DateLoader(CLoader): else: raise e.InterfaceError(f"unexpected DateStyle: {ds.decode('ascii')}") + cdef object _error_date(self, const char *data, str msg): + s = bytes(data).decode("utf8", "replace") + if s == "infinity" or len(s.split()[0]) > 10: + raise e.DataError(f"date too large (after year 10K): {s!r}") from None + elif s == "-infinity" or "BC" in s: + raise e.DataError(f"date too small (before year 1): {s!r}") from None + else: + raise e.DataError(f"can't parse date {s!r}: {msg}") from None + cdef object cload(self, const char *data, size_t length): if length != 10: - s = bytes(data).decode("utf8", "replace") - raise e.DataError(f"date not supported: {s!r}") + self._error_date(data, "unexpected length") DEF NVALUES = 3 cdef int vals[NVALUES] @@ -389,8 +397,7 @@ cdef class DateLoader(CLoader): else: return cdt.date_new(vals[2], vals[0], vals[1]) except ValueError as ex: - s = bytes(data).decode("utf8", "replace") - raise e.DataError(f"can't parse date {s!r}: {ex}") from None + self._error_date(data, str(ex)) @cython.final diff --git a/tests/types/test_datetime.py b/tests/types/test_datetime.py index 6b5d7daf1..977ad67b6 100644 --- a/tests/types/test_datetime.py +++ b/tests/types/test_datetime.py @@ -80,6 +80,31 @@ class TestDate: with pytest.raises(DataError): cur.fetchone()[0] + overflow_samples = [ + ("-infinity", "date too small"), + ("1000-01-01 BC", "date too small"), + ("10000-01-01", "date too large"), + ("infinity", "date too large"), + ] + + @pytest.mark.parametrize("datestyle_out", ["ISO", "Postgres", "SQL", "German"]) + @pytest.mark.parametrize("val, msg", overflow_samples) + def test_load_overflow_message(self, conn, datestyle_out, val, msg): + cur = conn.cursor() + cur.execute(f"set datestyle = {datestyle_out}, YMD") + cur.execute("select %s::date", (val,)) + with pytest.raises(DataError) as excinfo: + cur.fetchone()[0] + assert msg in str(excinfo.value) + + @pytest.mark.parametrize("val, msg", overflow_samples) + def test_load_overflow_message_binary(self, conn, val, msg): + cur = conn.cursor(binary=True) + cur.execute("select %s::date", (val,)) + with pytest.raises(DataError) as excinfo: + cur.fetchone()[0] + assert msg in str(excinfo.value) + def test_infinity_date_example(self, conn): # NOTE: this is an example in the docs. Make sure it doesn't regress when # adding binary datetime adapters