diff --git a/psycopg/connection_type.c b/psycopg/connection_type.c index 1b476c6c..08037079 100644 --- a/psycopg/connection_type.c +++ b/psycopg/connection_type.c @@ -52,75 +52,70 @@ static PyObject * psyco_conn_cursor(connectionObject *self, PyObject *args, PyObject *kwargs) { - PyObject *obj; + PyObject *obj = NULL; + PyObject *rv = NULL; PyObject *name = Py_None; PyObject *factory = (PyObject *)&cursorType; PyObject *withhold = Py_False; - PyObject *scrollable = Py_False; + PyObject *scrollable = Py_None; static char *kwlist[] = { "name", "cursor_factory", "withhold", "scrollable", NULL}; + EXC_IF_CONN_CLOSED(self); + if (!PyArg_ParseTupleAndKeywords( args, kwargs, "|OOOO", kwlist, &name, &factory, &withhold, &scrollable)) { - return NULL; + goto exit; } - if (PyObject_IsTrue(withhold) && (name == Py_None)) { - PyErr_SetString(ProgrammingError, - "'withhold=True can be specified only for named cursors"); - return NULL; - } - - if (PyObject_IsTrue(scrollable) && (name == Py_None)) { - PyErr_SetString(ProgrammingError, - "'scrollable=True can be specified only for named cursors"); - return NULL; - } - - EXC_IF_CONN_CLOSED(self); - if (self->status != CONN_STATUS_READY && self->status != CONN_STATUS_BEGIN && self->status != CONN_STATUS_PREPARED) { PyErr_SetString(OperationalError, "asynchronous connection attempt underway"); - return NULL; + goto exit; } if (name != Py_None && self->async == 1) { PyErr_SetString(ProgrammingError, "asynchronous connections " "cannot produce named cursors"); - return NULL; + goto exit; } Dprintf("psyco_conn_cursor: new %s cursor for connection at %p", (name == Py_None ? "unnamed" : "named"), self); if (!(obj = PyObject_CallFunctionObjArgs(factory, self, name, NULL))) { - return NULL; + goto exit; } if (PyObject_IsInstance(obj, (PyObject *)&cursorType) == 0) { PyErr_SetString(PyExc_TypeError, "cursor factory must be subclass of psycopg2._psycopg.cursor"); - Py_DECREF(obj); - return NULL; + goto exit; } - if (PyObject_IsTrue(withhold)) - ((cursorObject*)obj)->withhold = 1; - - if (PyObject_IsTrue(scrollable)) - ((cursorObject*)obj)->scrollable = 1; + if (0 != psyco_curs_withhold_set((cursorObject *)obj, withhold)) { + goto exit; + } + if (0 != psyco_curs_scrollable_set((cursorObject *)obj, scrollable)) { + goto exit; + } Dprintf("psyco_conn_cursor: new cursor at %p: refcnt = " FORMAT_CODE_PY_SSIZE_T, obj, Py_REFCNT(obj) - ); - return obj; + ); + + rv = obj; + obj = NULL; + +exit: + Py_XDECREF(obj); + return rv; } diff --git a/psycopg/cursor.h b/psycopg/cursor.h index e234f29c..7940e7b4 100644 --- a/psycopg/cursor.h +++ b/psycopg/cursor.h @@ -43,7 +43,11 @@ struct cursorObject { int closed:1; /* 1 if the cursor is closed */ int notuples:1; /* 1 if the command was not a SELECT query */ int withhold:1; /* 1 if the cursor is named and uses WITH HOLD */ - int scrollable:1; /* 1 if the cursor is named as SCROLLABLE */ + + int scrollable; /* 1 if the cursor is named and SCROLLABLE, + 0 if not scrollable + -1 if undefined (PG may decide scrollable or not) + */ long int rowcount; /* number of rows affected by last execute */ long int columns; /* number of columns fetched from the db */ @@ -85,9 +89,11 @@ struct cursorObject { }; -/* C-callable functions in cursor_int.c and cursor_ext.c */ +/* C-callable functions in cursor_int.c and cursor_type.c */ BORROWED HIDDEN PyObject *curs_get_cast(cursorObject *self, PyObject *oid); HIDDEN void curs_reset(cursorObject *self); +HIDDEN int psyco_curs_withhold_set(cursorObject *self, PyObject *pyvalue); +HIDDEN int psyco_curs_scrollable_set(cursorObject *self, PyObject *pyvalue); /* exception-raising macros */ #define EXC_IF_CURS_CLOSED(self) \ diff --git a/psycopg/cursor_type.c b/psycopg/cursor_type.c index 50fe8211..ff5c7515 100644 --- a/psycopg/cursor_type.c +++ b/psycopg/cursor_type.c @@ -370,6 +370,7 @@ _psyco_curs_execute(cursorObject *self, int res = -1; int tmp; PyObject *fquery, *cvt = NULL; + const char *scroll; operation = _psyco_curs_validate_sql_basic(self, operation); @@ -396,6 +397,21 @@ _psyco_curs_execute(cursorObject *self, if (0 > _mogrify(vars, operation, self, &cvt)) { goto exit; } } + switch (self->scrollable) { + case -1: + scroll = ""; + break; + case 0: + scroll = "NO SCROLL "; + break; + case 1: + scroll = "SCROLL "; + break; + default: + PyErr_SetString(InternalError, "unexpected scrollable value"); + goto exit; + } + if (vars && cvt) { if (!(fquery = _psyco_curs_merge_query_args(self, operation, cvt))) { goto exit; @@ -403,9 +419,9 @@ _psyco_curs_execute(cursorObject *self, if (self->name != NULL) { self->query = Bytes_FromFormat( - "DECLARE \"%s\" %sSCROLL CURSOR %s HOLD FOR %s", + "DECLARE \"%s\" %sCURSOR %s HOLD FOR %s", self->name, - self->scrollable ? "":"NO ", + scroll, self->withhold ? "WITH" : "WITHOUT", Bytes_AS_STRING(fquery)); Py_DECREF(fquery); @@ -417,9 +433,9 @@ _psyco_curs_execute(cursorObject *self, else { if (self->name != NULL) { self->query = Bytes_FromFormat( - "DECLARE \"%s\" %sSCROLL CURSOR %s HOLD FOR %s", + "DECLARE \"%s\" %sCURSOR %s HOLD FOR %s", self->name, - self->scrollable ? "":"NO ", + scroll, self->withhold ? "WITH" : "WITHOUT", Bytes_AS_STRING(operation)); } @@ -1567,12 +1583,12 @@ psyco_curs_withhold_get(cursorObject *self) return ret; } -static int +int psyco_curs_withhold_set(cursorObject *self, PyObject *pyvalue) { int value; - if (self->name == NULL) { + if (pyvalue != Py_False && self->name == NULL) { PyErr_SetString(ProgrammingError, "trying to set .withhold on unnamed cursor"); return -1; @@ -1592,25 +1608,42 @@ psyco_curs_withhold_set(cursorObject *self, PyObject *pyvalue) static PyObject * psyco_curs_scrollable_get(cursorObject *self) { - PyObject *ret; - ret = self->scrollable ? Py_True : Py_False; - Py_INCREF(ret); + PyObject *ret = NULL; + + switch (self->scrollable) { + case -1: + ret = Py_None; + break; + case 0: + ret = Py_False; + break; + case 1: + ret = Py_True; + break; + default: + PyErr_SetString(InternalError, "unexpected scrollable value"); + } + + Py_XINCREF(ret); return ret; } -static int +int psyco_curs_scrollable_set(cursorObject *self, PyObject *pyvalue) { int value; - if (self->name == NULL) { + if (pyvalue != Py_None && self->name == NULL) { PyErr_SetString(ProgrammingError, "trying to set .scrollable on unnamed cursor"); return -1; } - if ((value = PyObject_IsTrue(pyvalue)) == -1) + if (pyvalue == Py_None) { + value = -1; + } else if ((value = PyObject_IsTrue(pyvalue)) == -1) { return -1; + } self->scrollable = value; diff --git a/tests/test_cursor.py b/tests/test_cursor.py index dc2d4660..e57dd5a9 100755 --- a/tests/test_cursor.py +++ b/tests/test_cursor.py @@ -226,7 +226,7 @@ class CursorTests(unittest.TestCase): for t in range(2): if not t: curs = self.conn.cursor("S") - self.assertEqual(curs.scrollable, False); + self.assertEqual(curs.scrollable, None); curs.scrollable = True else: curs = self.conn.cursor("S", scrollable=True) @@ -252,6 +252,32 @@ class CursorTests(unittest.TestCase): curs.close() + def test_not_scrollable(self): + self.assertRaises(psycopg2.ProgrammingError, self.conn.cursor, + scrollable=False) + + curs = self.conn.cursor() + curs.execute("create table scrollable (data int)") + curs.executemany("insert into scrollable values (%s)", + [ (i,) for i in range(100) ]) + curs.close() + + curs = self.conn.cursor("S") # default scrollability + curs.execute("select * from scrollable") + self.assertEqual(curs.scrollable, None) + curs.scroll(2) + try: + curs.scroll(-1) + except psycopg2.OperationalError: + return self.skipTest("can't evaluate non-scrollable cursor") + curs.close() + + curs = self.conn.cursor("S", scrollable=False) + self.assertEqual(curs.scrollable, False) + curs.execute("select * from scrollable") + curs.scroll(2) + self.assertRaises(psycopg2.OperationalError, curs.scroll, -1) + @skip_before_postgres(8, 2) def test_iter_named_cursor_efficient(self): curs = self.conn.cursor('tmp')