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.0.15~3^2~3 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=a716049d5c6a3c5664ec90866cafd3069e32e7aa;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 e3b54a7a4..8335dba95 100644 --- a/psycopg/psycopg/types/datetime.py +++ b/psycopg/psycopg/types/datetime.py @@ -260,11 +260,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): @@ -275,7 +278,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 946317c0c..837a9049a 100644 --- a/psycopg_c/psycopg_c/types/datetime.pyx +++ b/psycopg_c/psycopg_c/types/datetime.pyx @@ -371,10 +371,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] @@ -395,8 +403,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 45cfca540..2a956e3fa 100644 --- a/tests/types/test_datetime.py +++ b/tests/types/test_datetime.py @@ -81,6 +81,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