summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniele Varrazzo <daniele.varrazzo@gmail.com>2017-06-16 19:41:52 +0100
committerDaniele Varrazzo <daniele.varrazzo@gmail.com>2017-06-16 19:41:52 +0100
commit2b5e1318316a064bbc598ab95d9b214fbda3c99e (patch)
tree9025e5bc8622485aaad2aecfbc955c381cc43eb1
parent324e56cfa354a737665068ffdecfb1fc17bb8c12 (diff)
parent70a2d2238e84ea8382bf994f2f79a008dbf5984d (diff)
downloadpsycopg2-2b5e1318316a064bbc598ab95d9b214fbda3c99e.tar.gz
Merge branch 'fix-558'
-rw-r--r--NEWS1
-rw-r--r--psycopg/typecast_datetime.c53
-rwxr-xr-xtests/test_dates.py21
3 files changed, 75 insertions, 0 deletions
diff --git a/NEWS b/NEWS
index 08b76f9..0a5de09 100644
--- a/NEWS
+++ b/NEWS
@@ -21,6 +21,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`).
- Added `~psycopg2.extras.Json` `!prepare()` method to consider connection
params when adapting (:ticket:`#562`).
- `~psycopg2.errorcodes` map updated to PostgreSQL 10 beta 1.
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..0b790d0 100755
--- a/tests/test_dates.py
+++ b/tests/test_dates.py
@@ -411,6 +411,27 @@ 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)),
+ ('1', timedelta(microseconds=1)),
+ ('-1', timedelta(microseconds=-1)),
+ ('1000000', timedelta(seconds=1)),
+ ('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'):