]> 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 22:18:17 +0000 (00:18 +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 e3b54a7a4ec29d629c582882489073575a724e84..8335dba9595faddd70a302571ea4469e7d3409b7 100644 (file)
@@ -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:
index 946317c0cf2bc01e0054e93a23f888522f371c26..837a9049a3990b9a388db83d3f8067de213a2a43 100644 (file)
@@ -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
index 45cfca540723525f7e56beff33e1459191f47bbe..2a956e3fad172b40990970245a7602410ea2215b 100644 (file)
@@ -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