]> git.ipfire.org Git - thirdparty/psycopg.git/commitdiff
fix: produce consistent error messages on date overflow
authorDaniele Varrazzo <daniele.varrazzo@gmail.com>
Wed, 8 Jun 2022 16:30:13 +0000 (18:30 +0200)
committerDaniele Varrazzo <daniele.varrazzo@gmail.com>
Wed, 8 Jun 2022 20:43:32 +0000 (22:43 +0200)
- 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

psycopg/psycopg/types/datetime.py
psycopg_c/psycopg_c/types/datetime.pyx
tests/types/test_datetime.py

index b11a7352240848e6a337c656a4dc5b069803ffd3..ac9217aaac93d109ab4409aa4b9a4db3e738306d 100644 (file)
@@ -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:
index 6ffa264477ab1cfd9db93af6d99a6a005e3b39c4..d84dc06990e7f5662ac3c3623bc4065da6e94c4c 100644 (file)
@@ -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
index 6b5d7daf1dd8460e1abc07ad3f245d38a2e578b7..977ad67b6fac94d154b47b4bd68f4afc775099f3 100644 (file)
@@ -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