Merge branch 'scrollable' into devel

This commit is contained in:
Daniele Varrazzo 2012-08-15 11:27:43 +01:00
commit 07e2c6a62f
8 changed files with 236 additions and 47 deletions

6
NEWS
View File

@ -1,6 +1,10 @@
What's new in psycopg 2.4.6 What's new in psycopg 2.4.6
--------------------------- ---------------------------
- Added support for backward scrollable cursors. Thanks to Jon Nelson
for the initial patch (ticket #108).
- connection.reset() implemented using DISCARD ALL on server versions
supporting it.
- Fixed 'cursor()' arguments propagation in connection subclasses - Fixed 'cursor()' arguments propagation in connection subclasses
and overriding of the 'cursor_factory' argument. Thanks to and overriding of the 'cursor_factory' argument. Thanks to
Corry Haines for the report and the initial patch (ticket #105). Corry Haines for the report and the initial patch (ticket #105).
@ -9,8 +13,6 @@ What's new in psycopg 2.4.6
Thanks to Manu Cupcic for the report (ticket #110). Thanks to Manu Cupcic for the report (ticket #110).
- 'register_hstore()', 'register_composite()', 'tpc_recover()' work with - 'register_hstore()', 'register_composite()', 'tpc_recover()' work with
RealDictConnection and Cursor (ticket #114). RealDictConnection and Cursor (ticket #114).
- connection.reset() implemented using DISCARD ALL on server versions
supporting it.
What's new in psycopg 2.4.5 What's new in psycopg 2.4.5

View File

@ -21,16 +21,17 @@ The ``connection`` class
Connections are thread safe and can be shared among many threads. See Connections are thread safe and can be shared among many threads. See
:ref:`thread-safety` for details. :ref:`thread-safety` for details.
.. method:: cursor([name] [, cursor_factory] [, withhold]) .. method:: cursor(name=None, cursor_factory=None, scrollable=None, withhold=False)
Return a new `cursor` object using the connection. Return a new `cursor` object using the connection.
If *name* is specified, the returned cursor will be a :ref:`server If *name* is specified, the returned cursor will be a :ref:`server
side cursor <server-side-cursors>` (also known as *named cursor*). side cursor <server-side-cursors>` (also known as *named cursor*).
Otherwise it will be a regular *client side* cursor. By default a Otherwise it will be a regular *client side* cursor. By default a
:sql:`WITHOUT HOLD` cursor is created; to create a :sql:`WITH HOLD` named cursor is declared without :sql:`SCROLL` option and
cursor, pass a `!True` value as the *withhold* parameter. See :sql:`WITHOUT HOLD`: set the argument or property `~cursor.scrollable`
:ref:`server-side-cursors`. to `!True`/`!False` and or `~cursor.withhold` to `!True` to change the
declaration.
The name can be a string not valid as a PostgreSQL identifier: for The name can be a string not valid as a PostgreSQL identifier: for
example it may start with a digit and contain non-alphanumeric example it may start with a digit and contain non-alphanumeric
@ -46,14 +47,16 @@ The ``connection`` class
Consider it as part of the query, not as a query parameter. Consider it as part of the query, not as a query parameter.
The *cursor_factory* argument can be used to create non-standard The *cursor_factory* argument can be used to create non-standard
cursors. The class returned should be a subclass of cursors. The class returned must be a subclass of
`psycopg2.extensions.cursor`. See :ref:`subclassing-cursor` for `psycopg2.extensions.cursor`. See :ref:`subclassing-cursor` for
details. details.
.. versionchanged:: 2.4.3 added the *withhold* argument.
.. versionchanged:: 2.4.6 added the *scrollable* argument.
.. extension:: .. extension::
The `name` and `cursor_factory` parameters are Psycopg All the function arguments are Psycopg extensions to the |DBAPI|.
extensions to the |DBAPI|.
.. index:: .. index::

View File

@ -114,13 +114,44 @@ The ``cursor`` class
The `name` attribute is a Psycopg extension to the |DBAPI|. The `name` attribute is a Psycopg extension to the |DBAPI|.
.. attribute:: scrollable
Read/write attribute: specifies if a named cursor is declared
:sql:`SCROLL`, hence is capable to scroll backwards (using
`~cursor.scroll()`). If `!True`, the cursor can be scrolled backwards,
if `!False` it is never scrollable. If `!None` (default) the cursor
scroll option is not specified, usually but not always meaning no
backward scroll (see the |declare-notes|__).
.. |declare-notes| replace:: :sql:`DECLARE` notes
.. __: http://www.postgresql.org/docs/current/static/sql-declare.html#SQL-DECLARE-NOTES
.. note::
set the value before calling `~cursor.execute()` or use the
`connection.cursor()` *scrollable* parameter, otherwise the value
will have no effect.
.. versionadded:: 2.4.6
.. extension::
The `scrollable` attribute is a Psycopg extension to the |DBAPI|.
.. attribute:: withhold .. attribute:: withhold
Read/write attribute: specifies if a named cursor lifetime should Read/write attribute: specifies if a named cursor lifetime should
extend outside of the current transaction, i.e., it is possible to extend outside of the current transaction, i.e., it is possible to
fetch from the cursor even after a `commection.commit()` (but not after fetch from the cursor even after a `connection.commit()` (but not after
a `connection.rollback()`). See :ref:`server-side-cursors` a `connection.rollback()`). See :ref:`server-side-cursors`
.. note::
set the value before calling `~cursor.execute()` or use the
`connection.cursor()` *withhold* parameter, otherwise the value
will have no effect.
.. versionadded:: 2.4.3 .. versionadded:: 2.4.3
.. extension:: .. extension::
@ -297,7 +328,8 @@ The ``cursor`` class
not changed. not changed.
The method can be used both for client-side cursors and The method can be used both for client-side cursors and
:ref:`server-side cursors <server-side-cursors>`. :ref:`server-side cursors <server-side-cursors>`. Server-side cursors
can usually scroll backwards only if declared `~cursor.scrollable`.
.. note:: .. note::

View File

@ -576,7 +576,9 @@ cursor is created using the `~connection.cursor()` method specifying the
*name* parameter. Such cursor will behave mostly like a regular cursor, *name* parameter. Such cursor will behave mostly like a regular cursor,
allowing the user to move in the dataset using the `~cursor.scroll()` allowing the user to move in the dataset using the `~cursor.scroll()`
method and to read the data using `~cursor.fetchone()` and method and to read the data using `~cursor.fetchone()` and
`~cursor.fetchmany()` methods. `~cursor.fetchmany()` methods. Normally you can only scroll forward in a
cursor: if you need to scroll backwards you should declare your cursor
`~cursor.scrollable`.
Named cursors are also :ref:`iterable <cursor-iterable>` like regular cursors. Named cursors are also :ref:`iterable <cursor-iterable>` like regular cursors.
Note however that before Psycopg 2.4 iteration was performed fetching one Note however that before Psycopg 2.4 iteration was performed fetching one

View File

@ -52,63 +52,70 @@
static PyObject * static PyObject *
psyco_conn_cursor(connectionObject *self, PyObject *args, PyObject *kwargs) psyco_conn_cursor(connectionObject *self, PyObject *args, PyObject *kwargs)
{ {
PyObject *obj; PyObject *obj = NULL;
PyObject *rv = NULL;
PyObject *name = Py_None; PyObject *name = Py_None;
PyObject *factory = (PyObject *)&cursorType; PyObject *factory = (PyObject *)&cursorType;
PyObject *withhold = Py_False; PyObject *withhold = Py_False;
PyObject *scrollable = Py_None;
static char *kwlist[] = {"name", "cursor_factory", "withhold", NULL}; static char *kwlist[] = {
"name", "cursor_factory", "withhold", "scrollable", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|OOO", kwlist,
&name, &factory, &withhold)) {
return NULL;
}
if (PyObject_IsTrue(withhold) && (name == Py_None)) {
PyErr_SetString(ProgrammingError,
"'withhold=True can be specified only for named cursors");
return NULL;
}
EXC_IF_CONN_CLOSED(self); EXC_IF_CONN_CLOSED(self);
if (!PyArg_ParseTupleAndKeywords(
args, kwargs, "|OOOO", kwlist,
&name, &factory, &withhold, &scrollable)) {
goto exit;
}
if (self->status != CONN_STATUS_READY && if (self->status != CONN_STATUS_READY &&
self->status != CONN_STATUS_BEGIN && self->status != CONN_STATUS_BEGIN &&
self->status != CONN_STATUS_PREPARED) { self->status != CONN_STATUS_PREPARED) {
PyErr_SetString(OperationalError, PyErr_SetString(OperationalError,
"asynchronous connection attempt underway"); "asynchronous connection attempt underway");
return NULL; goto exit;
} }
if (name != Py_None && self->async == 1) { if (name != Py_None && self->async == 1) {
PyErr_SetString(ProgrammingError, PyErr_SetString(ProgrammingError,
"asynchronous connections " "asynchronous connections "
"cannot produce named cursors"); "cannot produce named cursors");
return NULL; goto exit;
} }
Dprintf("psyco_conn_cursor: new %s cursor for connection at %p", Dprintf("psyco_conn_cursor: new %s cursor for connection at %p",
(name == Py_None ? "unnamed" : "named"), self); (name == Py_None ? "unnamed" : "named"), self);
if (!(obj = PyObject_CallFunctionObjArgs(factory, self, name, NULL))) { if (!(obj = PyObject_CallFunctionObjArgs(factory, self, name, NULL))) {
return NULL; goto exit;
} }
if (PyObject_IsInstance(obj, (PyObject *)&cursorType) == 0) { if (PyObject_IsInstance(obj, (PyObject *)&cursorType) == 0) {
PyErr_SetString(PyExc_TypeError, PyErr_SetString(PyExc_TypeError,
"cursor factory must be subclass of psycopg2._psycopg.cursor"); "cursor factory must be subclass of psycopg2._psycopg.cursor");
Py_DECREF(obj); goto exit;
return NULL;
} }
if (PyObject_IsTrue(withhold)) if (0 != psyco_curs_withhold_set((cursorObject *)obj, withhold)) {
((cursorObject*)obj)->withhold = 1; goto exit;
}
if (0 != psyco_curs_scrollable_set((cursorObject *)obj, scrollable)) {
goto exit;
}
Dprintf("psyco_conn_cursor: new cursor at %p: refcnt = " Dprintf("psyco_conn_cursor: new cursor at %p: refcnt = "
FORMAT_CODE_PY_SSIZE_T, FORMAT_CODE_PY_SSIZE_T,
obj, Py_REFCNT(obj) obj, Py_REFCNT(obj)
); );
return obj;
rv = obj;
obj = NULL;
exit:
Py_XDECREF(obj);
return rv;
} }

View File

@ -44,6 +44,11 @@ struct cursorObject {
int notuples:1; /* 1 if the command was not a SELECT query */ 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 withhold:1; /* 1 if the cursor is named and uses WITH HOLD */
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 rowcount; /* number of rows affected by last execute */
long int columns; /* number of columns fetched from the db */ long int columns; /* number of columns fetched from the db */
long int arraysize; /* how many rows should fetchmany() return */ long int arraysize; /* how many rows should fetchmany() return */
@ -84,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); BORROWED HIDDEN PyObject *curs_get_cast(cursorObject *self, PyObject *oid);
HIDDEN void curs_reset(cursorObject *self); 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 */ /* exception-raising macros */
#define EXC_IF_CURS_CLOSED(self) \ #define EXC_IF_CURS_CLOSED(self) \

View File

@ -370,6 +370,7 @@ _psyco_curs_execute(cursorObject *self,
int res = -1; int res = -1;
int tmp; int tmp;
PyObject *fquery, *cvt = NULL; PyObject *fquery, *cvt = NULL;
const char *scroll;
operation = _psyco_curs_validate_sql_basic(self, operation); 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; } 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 (vars && cvt) {
if (!(fquery = _psyco_curs_merge_query_args(self, operation, cvt))) { if (!(fquery = _psyco_curs_merge_query_args(self, operation, cvt))) {
goto exit; goto exit;
@ -403,8 +419,9 @@ _psyco_curs_execute(cursorObject *self,
if (self->name != NULL) { if (self->name != NULL) {
self->query = Bytes_FromFormat( self->query = Bytes_FromFormat(
"DECLARE \"%s\" CURSOR %s HOLD FOR %s", "DECLARE \"%s\" %sCURSOR %s HOLD FOR %s",
self->name, self->name,
scroll,
self->withhold ? "WITH" : "WITHOUT", self->withhold ? "WITH" : "WITHOUT",
Bytes_AS_STRING(fquery)); Bytes_AS_STRING(fquery));
Py_DECREF(fquery); Py_DECREF(fquery);
@ -416,8 +433,9 @@ _psyco_curs_execute(cursorObject *self,
else { else {
if (self->name != NULL) { if (self->name != NULL) {
self->query = Bytes_FromFormat( self->query = Bytes_FromFormat(
"DECLARE \"%s\" CURSOR %s HOLD FOR %s", "DECLARE \"%s\" %sCURSOR %s HOLD FOR %s",
self->name, self->name,
scroll,
self->withhold ? "WITH" : "WITHOUT", self->withhold ? "WITH" : "WITHOUT",
Bytes_AS_STRING(operation)); Bytes_AS_STRING(operation));
} }
@ -1565,12 +1583,12 @@ psyco_curs_withhold_get(cursorObject *self)
return ret; return ret;
} }
static int int
psyco_curs_withhold_set(cursorObject *self, PyObject *pyvalue) psyco_curs_withhold_set(cursorObject *self, PyObject *pyvalue)
{ {
int value; int value;
if (self->name == NULL) { if (pyvalue != Py_False && self->name == NULL) {
PyErr_SetString(ProgrammingError, PyErr_SetString(ProgrammingError,
"trying to set .withhold on unnamed cursor"); "trying to set .withhold on unnamed cursor");
return -1; return -1;
@ -1584,6 +1602,54 @@ psyco_curs_withhold_set(cursorObject *self, PyObject *pyvalue)
return 0; return 0;
} }
#define psyco_curs_scrollable_doc \
"Set or return cursor use of SCROLL"
static PyObject *
psyco_curs_scrollable_get(cursorObject *self)
{
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;
}
int
psyco_curs_scrollable_set(cursorObject *self, PyObject *pyvalue)
{
int value;
if (pyvalue != Py_None && self->name == NULL) {
PyErr_SetString(ProgrammingError,
"trying to set .scrollable on unnamed cursor");
return -1;
}
if (pyvalue == Py_None) {
value = -1;
} else if ((value = PyObject_IsTrue(pyvalue)) == -1) {
return -1;
}
self->scrollable = value;
return 0;
}
#endif #endif
@ -1710,6 +1776,10 @@ static struct PyGetSetDef cursorObject_getsets[] = {
(getter)psyco_curs_withhold_get, (getter)psyco_curs_withhold_get,
(setter)psyco_curs_withhold_set, (setter)psyco_curs_withhold_set,
psyco_curs_withhold_doc, NULL }, psyco_curs_withhold_doc, NULL },
{ "scrollable",
(getter)psyco_curs_scrollable_get,
(setter)psyco_curs_scrollable_set,
psyco_curs_scrollable_doc, NULL },
#endif #endif
{NULL} {NULL}
}; };
@ -1740,6 +1810,7 @@ cursor_setup(cursorObject *self, connectionObject *conn, const char *name)
self->closed = 0; self->closed = 0;
self->withhold = 0; self->withhold = 0;
self->scrollable = 0;
self->mark = conn->mark; self->mark = conn->mark;
self->pgres = NULL; self->pgres = NULL;
self->notuples = 1; self->notuples = 1;

View File

@ -213,6 +213,71 @@ class CursorTests(unittest.TestCase):
curs.execute("drop table withhold") curs.execute("drop table withhold")
self.conn.commit() self.conn.commit()
def test_scrollable(self):
self.assertRaises(psycopg2.ProgrammingError, self.conn.cursor,
scrollable=True)
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()
for t in range(2):
if not t:
curs = self.conn.cursor("S")
self.assertEqual(curs.scrollable, None);
curs.scrollable = True
else:
curs = self.conn.cursor("S", scrollable=True)
self.assertEqual(curs.scrollable, True);
curs.itersize = 10
# complex enough to make postgres cursors declare without
# scroll/no scroll to fail
curs.execute("""
select x.data
from scrollable x
join scrollable y on x.data = y.data
order by y.data""")
for i, (n,) in enumerate(curs):
self.assertEqual(i, n)
curs.scroll(-1)
for i in range(99, -1, -1):
curs.scroll(-1)
self.assertEqual(i, curs.fetchone()[0])
curs.scroll(-1)
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) @skip_before_postgres(8, 2)
def test_iter_named_cursor_efficient(self): def test_iter_named_cursor_efficient(self):
curs = self.conn.cursor('tmp') curs = self.conn.cursor('tmp')