Added support for scrollable cursors.

This commit is contained in:
Anthony Tuininga 2016-02-05 17:19:40 -07:00
parent 150303d995
commit 3eeff96403
6 changed files with 338 additions and 14 deletions

View File

@ -53,7 +53,7 @@ static PyObject *Connection_Commit(udt_Connection*, PyObject*);
static PyObject *Connection_Begin(udt_Connection*, PyObject*);
static PyObject *Connection_Prepare(udt_Connection*, PyObject*);
static PyObject *Connection_Rollback(udt_Connection*, PyObject*);
static PyObject *Connection_NewCursor(udt_Connection*, PyObject*);
static PyObject *Connection_NewCursor(udt_Connection*, PyObject*, PyObject*);
static PyObject *Connection_Cancel(udt_Connection*, PyObject*);
static PyObject *Connection_RegisterCallback(udt_Connection*, PyObject*);
static PyObject *Connection_UnregisterCallback(udt_Connection*, PyObject*);
@ -87,7 +87,8 @@ static PyObject *Connection_GetLTXID(udt_Connection*, void*);
// declaration of methods for Python type "Connection"
//-----------------------------------------------------------------------------
static PyMethodDef g_ConnectionMethods[] = {
{ "cursor", (PyCFunction) Connection_NewCursor, METH_NOARGS },
{ "cursor", (PyCFunction) Connection_NewCursor,
METH_VARARGS | METH_KEYWORDS },
{ "commit", (PyCFunction) Connection_Commit, METH_NOARGS },
{ "rollback", (PyCFunction) Connection_Rollback, METH_NOARGS },
{ "begin", (PyCFunction) Connection_Begin, METH_VARARGS },
@ -1102,7 +1103,7 @@ static PyObject *Connection_GetVersion(
}
// allocate a cursor to retrieve the version
cursor = (udt_Cursor*) Connection_NewCursor(self, NULL);
cursor = (udt_Cursor*) Connection_NewCursor(self, NULL, NULL);
if (!cursor)
return NULL;
@ -1489,16 +1490,23 @@ static PyObject *Connection_Rollback(
//-----------------------------------------------------------------------------
static PyObject *Connection_NewCursor(
udt_Connection *self, // connection to create cursor on
PyObject *args) // arguments
PyObject *args, // arguments
PyObject *keywordArgs) // keyword arguments
{
PyObject *createArgs, *result;
Py_ssize_t numArgs = 0, i;
createArgs = PyTuple_New(1);
if (args)
numArgs = PyTuple_GET_SIZE(args);
createArgs = PyTuple_New(1 + numArgs);
if (!createArgs)
return NULL;
Py_INCREF(self);
PyTuple_SET_ITEM(createArgs, 0, (PyObject*) self);
result = PyObject_Call( (PyObject*) &g_CursorType, createArgs, NULL);
for (i = 0; i < numArgs; i++)
PyTuple_SET_ITEM(createArgs, i + 1, PyTuple_GET_ITEM(args, i));
result = PyObject_Call( (PyObject*) &g_CursorType, createArgs,
keywordArgs);
Py_DECREF(createArgs);
return result;
}

146
Cursor.c
View File

@ -32,6 +32,7 @@ typedef struct {
int hasRowsToFetch;
int isOpen;
int isOwned;
boolean isScrollable;
} udt_Cursor;
@ -58,6 +59,7 @@ static PyObject *Cursor_FetchAll(udt_Cursor*, PyObject*);
static PyObject *Cursor_FetchRaw(udt_Cursor*, PyObject*, PyObject*);
static PyObject *Cursor_Parse(udt_Cursor*, PyObject*);
static PyObject *Cursor_Prepare(udt_Cursor*, PyObject*);
static PyObject *Cursor_Scroll(udt_Cursor*, PyObject*, PyObject*);
static PyObject *Cursor_SetInputSizes(udt_Cursor*, PyObject*, PyObject*);
static PyObject *Cursor_SetOutputSize(udt_Cursor*, PyObject*);
static PyObject *Cursor_Var(udt_Cursor*, PyObject*, PyObject*);
@ -98,6 +100,7 @@ static PyMethodDef g_CursorMethods[] = {
{ "executemanyprepared", (PyCFunction) Cursor_ExecuteManyPrepared,
METH_VARARGS },
{ "setoutputsize", (PyCFunction) Cursor_SetOutputSize, METH_VARARGS },
{ "scroll", (PyCFunction) Cursor_Scroll, METH_VARARGS | METH_KEYWORDS },
{ "var", (PyCFunction) Cursor_Var, METH_VARARGS | METH_KEYWORDS },
{ "arrayvar", (PyCFunction) Cursor_ArrayVar, METH_VARARGS },
{ "bindnames", (PyCFunction) Cursor_BindNames, METH_NOARGS },
@ -130,6 +133,7 @@ static PyMemberDef g_CursorMembers[] = {
0 },
{ "outputtypehandler", T_OBJECT, offsetof(udt_Cursor, outputTypeHandler),
0 },
{ "scrollable", T_BOOL, offsetof(udt_Cursor, isScrollable), 0 },
{ NULL }
};
@ -287,10 +291,14 @@ static int Cursor_Init(
PyObject *args, // arguments
PyObject *keywordArgs) // keyword arguments
{
static char *keywordList[] = { "connection", "scrollable", NULL };
udt_Connection *connection;
PyObject *scrollableObj;
// parse arguments
if (!PyArg_ParseTuple(args, "O!", &g_ConnectionType, &connection))
scrollableObj = NULL;
if (!PyArg_ParseTupleAndKeywords(args, keywordArgs, "O!|O", keywordList,
&g_ConnectionType, &connection, &scrollableObj))
return -1;
// initialize members
@ -304,6 +312,11 @@ static int Cursor_Init(
self->outputSize = -1;
self->outputSizeColumn = -1;
self->isOpen = 1;
if (scrollableObj) {
self->isScrollable = PyObject_IsTrue(scrollableObj);
if (self->isScrollable < 0)
return -1;
}
return 0;
}
@ -1637,6 +1650,7 @@ static PyObject *Cursor_Execute(
{
PyObject *statement, *executeArgs;
int isQuery;
ub4 mode;
executeArgs = NULL;
if (!PyArg_ParseTuple(args, "O|O", &statement, &executeArgs))
@ -1676,8 +1690,11 @@ static PyObject *Cursor_Execute(
return NULL;
// execute the statement
mode = OCI_DEFAULT;
isQuery = (self->statementType == OCI_STMT_SELECT);
if (Cursor_InternalExecute(self, isQuery ? 0 : 1, 0) < 0)
if (isQuery && self->isScrollable)
mode = OCI_STMT_SCROLLABLE_READONLY;
if (Cursor_InternalExecute(self, isQuery ? 0 : 1, mode) < 0)
return NULL;
// perform defines, if necessary
@ -1851,9 +1868,9 @@ static int Cursor_InternalFetch(
udt_Cursor *self, // cursor to fetch from
int numRows) // number of rows to fetch
{
ub4 currentPosition;
udt_Variable *var;
sword status;
ub8 rowCount;
int i;
if (!self->fetchVariables) {
@ -1878,14 +1895,22 @@ static int Cursor_InternalFetch(
"Cursor_InternalFetch(): fetch") < 0)
return -1;
// determine the number of rows fetched into buffers
status = OCIAttrGet(self->handle, OCI_HTYPE_STMT, &self->bufferRowCount, 0,
OCI_ATTR_ROWS_FETCHED, self->environment->errorHandle);
if (Environment_CheckForError(self->environment, status,
"Cursor_InternalFetch(): get rows fetched") < 0)
return -1;
if (Cursor_GetRowCount(self, &rowCount) < 0)
// determine the current position in the cursor
status = OCIAttrGet(self->handle, OCI_HTYPE_STMT, &currentPosition, 0,
OCI_ATTR_CURRENT_POSITION, self->environment->errorHandle);
if (Environment_CheckForError(self->environment, status,
"Cursor_InternalFetch(): get current position") < 0)
return -1;
self->rowCount = rowCount - self->bufferRowCount;
// reset buffer row index and row count
self->rowCount = currentPosition - self->bufferRowCount;
self->bufferRowIndex = 0;
return 0;
@ -2061,6 +2086,115 @@ static PyObject *Cursor_FetchRaw(
}
//-----------------------------------------------------------------------------
// Cursor_Scroll()
// Scroll the cursor using the value and mode specified.
//-----------------------------------------------------------------------------
static PyObject *Cursor_Scroll(
udt_Cursor *self, // cursor to execute
PyObject *args, // arguments
PyObject *keywordArgs) // keyword arguments
{
static char *keywordList[] = { "value", "mode", NULL };
ub8 desiredRow, minRowInBuffers, maxRowInBuffers;
ub4 fetchMode, numRows, currentPosition;
sword status;
char *mode;
int value;
// parse arguments
value = 0;
mode = NULL;
if (!PyArg_ParseTupleAndKeywords(args, keywordArgs, "|is", keywordList,
&value, &mode))
return NULL;
// validate mode
if (!mode) {
fetchMode = OCI_FETCH_RELATIVE;
desiredRow = self->rowCount + value;
} else if (strcmp(mode, "relative") == 0) {
fetchMode = OCI_FETCH_RELATIVE;
desiredRow = self->rowCount + value;
} else if (strcmp(mode, "absolute") == 0) {
fetchMode = OCI_FETCH_ABSOLUTE;
desiredRow = value;
} else if (strcmp(mode, "first") == 0) {
fetchMode = OCI_FETCH_FIRST;
desiredRow = 1;
} else if (strcmp(mode, "last") == 0) {
fetchMode = OCI_FETCH_LAST;
desiredRow = 0;
} else {
PyErr_SetString(g_InterfaceErrorException,
"mode must be one of relative, absolute, first or last");
return NULL;
}
// make sure the cursor is open
if (Cursor_IsOpen(self) < 0)
return NULL;
// determine if a fetch is actually required; "last" is always fetched
if (fetchMode != OCI_FETCH_LAST && self->bufferRowCount > 0) {
minRowInBuffers = self->rowCount - self->bufferRowIndex;
maxRowInBuffers = self->rowCount + self->bufferRowCount -
self->bufferRowIndex - 1;
if (self->bufferRowIndex == self->bufferRowCount) {
minRowInBuffers += 1;
maxRowInBuffers += 1;
}
if (desiredRow >= minRowInBuffers && desiredRow <= maxRowInBuffers) {
self->bufferRowIndex = desiredRow - minRowInBuffers;
self->rowCount = desiredRow - 1;
Py_RETURN_NONE;
}
}
// perform fetch; when fetching the last row, only fetch a single row
numRows = (fetchMode == OCI_FETCH_LAST) ? 1 : self->fetchArraySize;
Py_BEGIN_ALLOW_THREADS
status = OCIStmtFetch2(self->handle, self->environment->errorHandle,
numRows, fetchMode, value, OCI_DEFAULT);
Py_END_ALLOW_THREADS
if (status == OCI_NO_DATA) {
if (fetchMode == OCI_FETCH_FIRST || fetchMode == OCI_FETCH_LAST) {
self->hasRowsToFetch = 0;
self->rowCount = 0;
self->bufferRowCount = 0;
self->bufferRowIndex = 0;
} else {
PyErr_SetString(PyExc_IndexError,
"requested scroll operation would leave result set");
return NULL;
}
} else if (Environment_CheckForError(self->environment, status,
"Cursor_Scroll(): fetch") < 0)
return NULL;
self->hasRowsToFetch = 1;
// determine the number of rows actually fetched
status = OCIAttrGet(self->handle, OCI_HTYPE_STMT, &self->bufferRowCount, 0,
OCI_ATTR_ROWS_FETCHED, self->environment->errorHandle);
if (Environment_CheckForError(self->environment, status,
"Cursor_Scroll(): get rows fetched") < 0)
return NULL;
// determine the current position of the cursor
status = OCIAttrGet(self->handle, OCI_HTYPE_STMT, &currentPosition, 0,
OCI_ATTR_CURRENT_POSITION, self->environment->errorHandle);
if (Environment_CheckForError(self->environment, status,
"Cursor_Scroll(): get current position") < 0)
return NULL;
// reset buffer row index and row count
self->rowCount = currentPosition - self->bufferRowCount;
self->bufferRowIndex = 0;
Py_RETURN_NONE;
}
//-----------------------------------------------------------------------------
// Cursor_SetInputSizes()
// Set the sizes of the bind variables.
@ -2515,7 +2649,7 @@ static PyObject * Cursor_GetImplicitResults(
// populate it with the implicit results
for (i = 0; i < numImplicitResults; i++) {
childCursor = (udt_Cursor*) Connection_NewCursor(self->connection,
NULL);
NULL, NULL);
if (!childCursor) {
Py_DECREF(result);
return NULL;

View File

@ -92,7 +92,8 @@ static int CursorVar_Initialize(
if (!var->cursors)
return -1;
for (i = 0; i < var->allocatedElements; i++) {
tempCursor = (udt_Cursor*) Connection_NewCursor(var->connection, NULL);
tempCursor = (udt_Cursor*) Connection_NewCursor(var->connection, NULL,
NULL);
if (!tempCursor) {
Py_DECREF(var);
return -1;

View File

@ -1007,7 +1007,7 @@ static PyObject *Subscription_RegisterQuery(
// create cursor to perform query
env = self->connection->environment;
cursor = (udt_Cursor*) Connection_NewCursor(self->connection, NULL);
cursor = (udt_Cursor*) Connection_NewCursor(self->connection, NULL, NULL);
if (!cursor)
return NULL;

View File

@ -408,6 +408,42 @@ Cursor Object
The DB API definition does not define this attribute.
.. method:: Cursor.scroll(value = 0, mode="relative")
Scroll the cursor in the result set to a new position according to the mode.
If mode is "relative" (the default value), the value is taken as an offset
to the current position in the result set. If set to "absolute", value
states an absolute target position. If set to "first", the cursor is
positioned at the first row and if set to "last", the cursor is set to the
last row in the result set.
An IndexError is raised if the mode is "relative" or "absolute" and the
scroll operation would position the cursor outside of the result set.
.. versionadded:: development
.. note::
This method is an extension to the DB API definition but it is
mentioned in PEP 249 as an optional extension.
.. attribute:: Cursor.scrollable
This read-write boolean attribute specifies whether the cursor can be
scrolled or not. By default, cursors are not scrollable, as the server
resources and response times are greater than nonscrollable cursors. This
attribute is checked and the corresponding mode set in Oracle when calling
the method :meth:`~Cursor.execute()`.
.. versionadded:: development
.. note::
The DB API definition does not define this attribute.
.. method:: Cursor.setinputsizes(\*args, \*\*keywordArgs)
This can be used before a call to execute(), callfunc() or callproc() to

View File

@ -5,6 +5,17 @@ import sys
class TestCursor(BaseTestCase):
def testCreateScrollableCursor(self):
"""test creating a scrollable cursor"""
cursor = self.connection.cursor()
self.assertEqual(cursor.scrollable, False)
cursor = self.connection.cursor(True)
self.assertEqual(cursor.scrollable, True)
cursor = self.connection.cursor(scrollable = True)
self.assertEqual(cursor.scrollable, True)
cursor.scrollable = False
self.assertEqual(cursor.scrollable, False)
def testExecuteNoArgs(self):
"""test executing a statement without any arguments"""
result = self.cursor.execute("begin null; end;")
@ -236,6 +247,140 @@ class TestCursor(BaseTestCase):
self.assertRaises(cx_Oracle.InterfaceError,
self.cursor.fetchall)
def testScrollAbsoluteExceptionAfter(self):
"""test scrolling absolute yields an exception (after result set)"""
cursor = self.connection.cursor(scrollable = True)
cursor.arraysize = self.cursor.arraysize
cursor.execute("""
select NumberCol
from TestNumbers
order by IntCol""")
self.assertRaises(IndexError, cursor.scroll, 12, "absolute")
def testScrollAbsoluteInBuffer(self):
"""test scrolling absolute (when in buffers)"""
cursor = self.connection.cursor(scrollable = True)
cursor.arraysize = self.cursor.arraysize
cursor.execute("""
select NumberCol
from TestNumbers
order by IntCol""")
cursor.fetchmany()
self.assertTrue(cursor.arraysize > 1,
"array size must exceed 1 for this test to work correctly")
cursor.scroll(1, mode = "absolute")
row = cursor.fetchone()
self.assertEqual(row[0], 1.25)
self.assertEqual(cursor.rowcount, 1)
def testScrollAbsoluteNotInBuffer(self):
"""test scrolling absolute (when not in buffers)"""
cursor = self.connection.cursor(scrollable = True)
cursor.arraysize = self.cursor.arraysize
cursor.execute("""
select NumberCol
from TestNumbers
order by IntCol""")
cursor.scroll(6, mode = "absolute")
row = cursor.fetchone()
self.assertEqual(row[0], 7.5)
self.assertEqual(cursor.rowcount, 6)
def testScrollFirstInBuffer(self):
"""test scrolling to first row in result set (when in buffers)"""
cursor = self.connection.cursor(scrollable = True)
cursor.arraysize = self.cursor.arraysize
cursor.execute("""
select NumberCol
from TestNumbers
order by IntCol""")
cursor.fetchmany()
cursor.scroll(mode = "first")
row = cursor.fetchone()
self.assertEqual(row[0], 1.25)
self.assertEqual(cursor.rowcount, 1)
def testScrollFirstNotInBuffer(self):
"""test scrolling to first row in result set (when not in buffers)"""
cursor = self.connection.cursor(scrollable = True)
cursor.arraysize = self.cursor.arraysize
cursor.execute("""
select NumberCol
from TestNumbers
order by IntCol""")
cursor.fetchmany()
cursor.fetchmany()
cursor.scroll(mode = "first")
row = cursor.fetchone()
self.assertEqual(row[0], 1.25)
self.assertEqual(cursor.rowcount, 1)
def testScrollLast(self):
"""test scrolling to last row in result set"""
cursor = self.connection.cursor(scrollable = True)
cursor.arraysize = self.cursor.arraysize
cursor.execute("""
select NumberCol
from TestNumbers
order by IntCol""")
cursor.scroll(mode = "last")
row = cursor.fetchone()
self.assertEqual(row[0], 12.5)
self.assertEqual(cursor.rowcount, 10)
def testScrollRelativeExceptionAfter(self):
"""test scrolling relative yields an exception (after result set)"""
cursor = self.connection.cursor(scrollable = True)
cursor.arraysize = self.cursor.arraysize
cursor.execute("""
select NumberCol
from TestNumbers
order by IntCol""")
self.assertRaises(IndexError, cursor.scroll, 15)
def testScrollRelativeExceptionBefore(self):
"""test scrolling relative yields an exception (before result set)"""
cursor = self.connection.cursor(scrollable = True)
cursor.arraysize = self.cursor.arraysize
cursor.execute("""
select NumberCol
from TestNumbers
order by IntCol""")
self.assertRaises(IndexError, cursor.scroll, -5)
def testScrollRelativeInBuffer(self):
"""test scrolling relative (when in buffers)"""
cursor = self.connection.cursor(scrollable = True)
cursor.arraysize = self.cursor.arraysize
cursor.execute("""
select NumberCol
from TestNumbers
order by IntCol""")
cursor.fetchmany()
self.assertTrue(cursor.arraysize > 1,
"array size must exceed 1 for this test to work correctly")
cursor.scroll(2 - cursor.rowcount)
row = cursor.fetchone()
self.assertEqual(row[0], 2.5)
self.assertEqual(cursor.rowcount, 2)
def testScrollRelativeNotInBuffer(self):
"""test scrolling relative (when not in buffers)"""
cursor = self.connection.cursor(scrollable = True)
cursor.arraysize = self.cursor.arraysize
cursor.execute("""
select NumberCol
from TestNumbers
order by IntCol""")
cursor.fetchmany()
cursor.fetchmany()
self.assertTrue(cursor.arraysize > 1,
"array size must exceed 2 for this test to work correctly")
cursor.scroll(3 - cursor.rowcount)
row = cursor.fetchone()
self.assertEqual(row[0], 3.75)
self.assertEqual(cursor.rowcount, 3)
def testSetInputSizesMultipleMethod(self):
"""test setting input sizes with both positional and keyword args"""
self.assertRaises(cx_Oracle.InterfaceError,