From 315f72862cb605b4e9f8d127dcc87bbffff6ffa0 Mon Sep 17 00:00:00 2001 From: Daniele Varrazzo Date: Fri, 16 Jun 2017 00:54:38 +0100 Subject: Parse a number as microseconds when casting interval Should close #558, but I'm curious to know if a number is returned for interval < 1 day too (which wouldn't trigger the overflow, but will finish parsing with part=0). --- psycopg/typecast_datetime.c | 53 +++++++++++++++++++++++++++++++++++++++++++++ tests/test_dates.py | 19 ++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/psycopg/typecast_datetime.c b/psycopg/typecast_datetime.c index fec57de..b0b257b 100644 --- a/psycopg/typecast_datetime.c +++ b/psycopg/typecast_datetime.c @@ -276,6 +276,44 @@ typecast_PYTIME_cast(const char *str, Py_ssize_t len, PyObject *curs) return obj; } + +/* Attempt parsing a number as microseconds + * Redshift is reported returning this stuff, see #558 + * + * Return a new `timedelta()` object in case of success or NULL and set an error + */ +static PyObject * +interval_from_usecs(const char *str) +{ + PyObject *us = NULL; + char *pend; + PyObject *rv = NULL; + + Dprintf("interval_from_usecs: %s", str); + + if (!(us = PyLong_FromString((char *)str, &pend, 0))) { + Dprintf("interval_from_usecs: parsing long failed"); + goto exit; + } + + if (*pend != '\0') { + /* there are trailing chars, it's not just micros. Barf. */ + Dprintf("interval_from_usecs: spurious chars %s", pend); + PyErr_Format(PyExc_ValueError, + "expected number of microseconds, got %s", str); + goto exit; + } + + rv = PyObject_CallFunction( + (PyObject*)PyDateTimeAPI->DeltaType, "LLO", + 0L, 0L, us); + +exit: + Py_XDECREF(us); + return rv; +} + + /** INTERVAL - parse an interval into a timedelta object **/ static PyObject * @@ -284,6 +322,7 @@ typecast_PYINTERVAL_cast(const char *str, Py_ssize_t len, PyObject *curs) long v = 0, years = 0, months = 0, hours = 0, minutes = 0, micros = 0; PY_LONG_LONG days = 0, seconds = 0; int sign = 1, denom = 1, part = 0; + const char *orig = str; if (str == NULL) { Py_RETURN_NONE; } @@ -305,6 +344,16 @@ typecast_PYINTERVAL_cast(const char *str, Py_ssize_t len, PyObject *curs) * or too big value. On Win where long == int the 2nd check * is useless. */ if (v1 < v || v1 > (long)INT_MAX) { + /* uhm, oops... but before giving up, maybe it's redshift + * returning microseconds? See #558 */ + PyObject *rv; + if ((rv = interval_from_usecs(orig))) { + return rv; + } + else { + PyErr_Clear(); + } + PyErr_SetString( PyExc_OverflowError, "interval component too big"); return NULL; @@ -384,6 +433,10 @@ typecast_PYINTERVAL_cast(const char *str, Py_ssize_t len, PyObject *curs) micros = (long)round((double)micros / denom * 1000000.0); } } + else if (part == 0) { + /* Parsing failed, maybe it's just an integer? Assume usecs */ + return interval_from_usecs(orig); + } /* add hour, minutes, seconds together and include the sign */ seconds += 60 * (PY_LONG_LONG)minutes + 3600 * (PY_LONG_LONG)hours; diff --git a/tests/test_dates.py b/tests/test_dates.py index 83eea32..f2f49f2 100755 --- a/tests/test_dates.py +++ b/tests/test_dates.py @@ -411,6 +411,25 @@ class DatetimeTests(ConnectingTestCase, CommonDatetimeTestsMixin): self.assert_(t.tzinfo is not None) self.assert_(t < datetime(1000, 1, 1, tzinfo=FixedOffsetTimezone())) + def test_redshift_day(self): + # Redshift is reported returning 1 day interval as microsec (bug #558) + cur = self.conn.cursor() + psycopg2.extensions.register_type( + psycopg2.extensions.new_type( + psycopg2.STRING.values, 'WAT', psycopg2.extensions.INTERVAL), + cur) + + from datetime import timedelta + for s, v in [ + ('0', timedelta(0)), + ('1000000', timedelta(seconds=1)), # Is this a thing? + ('86400000000', timedelta(days=1)), + ('-86400000000', timedelta(days=-1)), + ]: + cur.execute("select %s::text", (s,)) + r = cur.fetchone()[0] + self.assertEqual(r, v, "%s -> %s != %s" % (s, r, v)) + # Only run the datetime tests if psycopg was compiled with support. if not hasattr(psycopg2.extensions, 'PYDATETIME'): -- cgit v1.2.1 From 70a2d2238e84ea8382bf994f2f79a008dbf5984d Mon Sep 17 00:00:00 2001 From: Daniele Varrazzo Date: Fri, 16 Jun 2017 19:39:18 +0100 Subject: Consider redshift interval supported after further tests --- NEWS | 1 + tests/test_dates.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 704c1b0..271f10b 100644 --- a/NEWS +++ b/NEWS @@ -19,6 +19,7 @@ What's new in psycopg 2.7.2 - Fixed `~psycopg2.extras.ReplicationCursor.consume_stream()` *keepalive_interval* argument (:ticket:`#547`). - Fixed random `!SystemError` upon receiving abort signal (:ticket:`#551`). +- Parse intervals returned as microseconds from Redshift (:ticket:`#558`). - `~psycopg2.errorcodes` map updated to PostgreSQL 10 beta 1. diff --git a/tests/test_dates.py b/tests/test_dates.py index f2f49f2..0b790d0 100755 --- a/tests/test_dates.py +++ b/tests/test_dates.py @@ -422,7 +422,9 @@ class DatetimeTests(ConnectingTestCase, CommonDatetimeTestsMixin): from datetime import timedelta for s, v in [ ('0', timedelta(0)), - ('1000000', timedelta(seconds=1)), # Is this a thing? + ('1', timedelta(microseconds=1)), + ('-1', timedelta(microseconds=-1)), + ('1000000', timedelta(seconds=1)), ('86400000000', timedelta(days=1)), ('-86400000000', timedelta(days=-1)), ]: -- cgit v1.2.1