//-----------------------------------------------------------------------------
// Copyright (c) 2016, 2020, Oracle and/or its affiliates. All rights reserved.
//
// Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved.
//
// Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta,
// Canada. All rights reserved.
//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
// cxoCursor.c
//   Definition of the Python type Cursor.
//-----------------------------------------------------------------------------

#include "cxoModule.h"

//-----------------------------------------------------------------------------
// cxoCursor_new()
//   Create a new cursor object.
//-----------------------------------------------------------------------------
static PyObject *cxoCursor_new(PyTypeObject *type, PyObject *args,
        PyObject *keywordArgs)
{
    return type->tp_alloc(type, 0);
}


//-----------------------------------------------------------------------------
// cxoCursor_init()
//   Create a new cursor object.
//-----------------------------------------------------------------------------
static int cxoCursor_init(cxoCursor *cursor, PyObject *args,
        PyObject *keywordArgs)
{
    static char *keywordList[] = { "connection", "scrollable", NULL };
    cxoConnection *connection;
    int isScrollable;

    // parse arguments
    isScrollable = 0;
    if (!PyArg_ParseTupleAndKeywords(args, keywordArgs, "O!|p", keywordList,
            &cxoPyTypeConnection, &connection, &isScrollable))
        return -1;
    cursor->isScrollable = (char) isScrollable;

    // initialize members
    Py_INCREF(connection);
    cursor->connection = connection;
    cursor->arraySize = 100;
    cursor->fetchArraySize = 100;
    cursor->prefetchRows = DPI_DEFAULT_PREFETCH_ROWS;
    cursor->bindArraySize = 1;
    cursor->isOpen = 1;

    return 0;
}


//-----------------------------------------------------------------------------
// cxoCursor_repr()
//   Return a string representation of the cursor.
//-----------------------------------------------------------------------------
static PyObject *cxoCursor_repr(cxoCursor *cursor)
{
    PyObject *connectionRepr, *module, *name, *result;

    connectionRepr = PyObject_Repr((PyObject*) cursor->connection);
    if (!connectionRepr)
        return NULL;
    if (cxoUtils_getModuleAndName(Py_TYPE(cursor), &module, &name) < 0) {
        Py_DECREF(connectionRepr);
        return NULL;
    }
    result = cxoUtils_formatString("<%s.%s on %s>",
            PyTuple_Pack(3, module, name, connectionRepr));
    Py_DECREF(module);
    Py_DECREF(name);
    Py_DECREF(connectionRepr);
    return result;
}


//-----------------------------------------------------------------------------
// cxoCursor_free()
//   Deallocate the cursor.
//-----------------------------------------------------------------------------
static void cxoCursor_free(cxoCursor *cursor)
{
    Py_CLEAR(cursor->statement);
    Py_CLEAR(cursor->statementTag);
    Py_CLEAR(cursor->bindVariables);
    Py_CLEAR(cursor->fetchVariables);
    if (cursor->handle) {
        dpiStmt_release(cursor->handle);
        cursor->handle = NULL;
    }
    Py_CLEAR(cursor->connection);
    Py_CLEAR(cursor->rowFactory);
    Py_CLEAR(cursor->inputTypeHandler);
    Py_CLEAR(cursor->outputTypeHandler);
    Py_TYPE(cursor)->tp_free((PyObject*) cursor);
}


//-----------------------------------------------------------------------------
// cxoCursor_isOpen()
//   Determines if the cursor object is open. Since the same cursor can be
// used to execute multiple statements, simply checking for the DPI statement
// handle is insufficient.
//-----------------------------------------------------------------------------
static int cxoCursor_isOpen(cxoCursor *cursor)
{
    if (!cursor->isOpen) {
        cxoError_raiseFromString(cxoInterfaceErrorException, "not open");
        return -1;
    }
    return cxoConnection_isConnected(cursor->connection);
}


//-----------------------------------------------------------------------------
// cxoCursor_fetchRow()
//   Fetch a single row from the cursor. Internally the number of rows left in
// the buffer is managed in order to minimize calls to Py_BEGIN_ALLOW_THREADS
// and Py_END_ALLOW_THREADS which have a significant overhead.
//-----------------------------------------------------------------------------
static int cxoCursor_fetchRow(cxoCursor *cursor, int *found,
        uint32_t *bufferRowIndex)
{
    int status;

    // if the number of rows in the fetch buffer is zero and there are more
    // rows to fetch, call DPI with threading enabled in order to perform any
    // fetch requiring a network round trip
    if (cursor->numRowsInFetchBuffer == 0 && cursor->moreRowsToFetch) {
        Py_BEGIN_ALLOW_THREADS
        status = dpiStmt_fetchRows(cursor->handle, cursor->fetchArraySize,
                &cursor->fetchBufferRowIndex, &cursor->numRowsInFetchBuffer,
                &cursor->moreRowsToFetch);
        Py_END_ALLOW_THREADS
        if (status < 0)
            return cxoError_raiseAndReturnInt();
    }

    // keep track of where we are in the fetch buffer
    if (cursor->numRowsInFetchBuffer == 0)
        *found = 0;
    else {
        *found = 1;
        *bufferRowIndex = cursor->fetchBufferRowIndex++;
        cursor->numRowsInFetchBuffer--;
    }

    return 0;
}


//-----------------------------------------------------------------------------
// cxoCursor_performDefine()
//   Perform the defines for the cursor. At this point it is assumed that the
// statement being executed is in fact a query.
//-----------------------------------------------------------------------------
static int cxoCursor_performDefine(cxoCursor *cursor, uint32_t numQueryColumns)
{
    PyObject *outputTypeHandler, *result;
    cxoTransformNum transformNum;
    cxoObjectType *objectType;
    dpiQueryInfo queryInfo;
    uint32_t pos, size;
    cxoDbType *dbType;
    char message[120];
    cxoVar *var;

    // initialize fetching variables; these are used to reduce the number of
    // times that Py_BEGIN_ALLOW_THREADS/Py_END_ALLOW_THREADS is called as
    // there is a significant amount of overhead in making these calls
    cursor->numRowsInFetchBuffer = 0;
    cursor->moreRowsToFetch = 1;

    // if fetch variables already exist, nothing more to do (we are executing
    // the same statement and therefore all defines have already been
    // performed)
    if (cursor->fetchVariables)
        return 0;

    // create a list corresponding to the number of items
    cursor->fetchVariables = PyList_New(numQueryColumns);
    if (!cursor->fetchVariables)
        return -1;

    // create a variable for each of the query columns
    cursor->fetchArraySize = cursor->arraySize;
    for (pos = 1; pos <= numQueryColumns; pos++) {

        // get query information for the column position
        if (dpiStmt_getQueryInfo(cursor->handle, pos, &queryInfo) < 0)
            return cxoError_raiseAndReturnInt();
        if (queryInfo.typeInfo.sizeInChars)
            size = queryInfo.typeInfo.sizeInChars;
        else size = queryInfo.typeInfo.clientSizeInBytes;

        // determine object type, if applicable
        objectType = NULL;
        if (queryInfo.typeInfo.objectType) {
            objectType = cxoObjectType_new(cursor->connection,
                    queryInfo.typeInfo.objectType);
            if (!objectType)
                return -1;
        }

        // determine the default types to use
        transformNum =
                cxoTransform_getNumFromDataTypeInfo(&queryInfo.typeInfo);
        if (transformNum == CXO_TRANSFORM_UNSUPPORTED) {
            snprintf(message, sizeof(message), "Oracle type %d not supported.",
                    queryInfo.typeInfo.oracleTypeNum);
            cxoError_raiseFromString(cxoNotSupportedErrorException, message);
            return -1;
        }
        dbType = cxoDbType_fromTransformNum(transformNum);
        if (!dbType)
            return -1;

        // see if an output type handler should be used
        var = NULL;
        outputTypeHandler = NULL;
        if (cursor->outputTypeHandler && cursor->outputTypeHandler != Py_None)
            outputTypeHandler = cursor->outputTypeHandler;
        else if (cursor->connection->outputTypeHandler &&
                cursor->connection->outputTypeHandler != Py_None)
            outputTypeHandler = cursor->connection->outputTypeHandler;

        // if using an output type handler, None implies default behavior
        if (outputTypeHandler) {
            result = PyObject_CallFunction(outputTypeHandler, "Os#Oiii",
                    cursor, queryInfo.name, (Py_ssize_t) queryInfo.nameLength,
                    dbType, size, queryInfo.typeInfo.precision,
                    queryInfo.typeInfo.scale);
            if (!result) {
                Py_XDECREF(objectType);
                return -1;
            } else if (result == Py_None)
                Py_DECREF(result);
            else if (!cxoVar_check(result)) {
                Py_DECREF(result);
                Py_XDECREF(objectType);
                PyErr_SetString(PyExc_TypeError,
                        "expecting variable from output type handler");
                return -1;
            } else {
                var = (cxoVar*) result;
                if (var->allocatedElements < cursor->fetchArraySize) {
                    Py_DECREF(result);
                    Py_XDECREF(objectType);
                    PyErr_SetString(PyExc_TypeError,
                            "expecting variable with array size large "
                            "enough for fetch");
                    return -1;
                }
            }
        }

        // if no variable created yet, use the database metadata
        if (!var) {
            var = cxoVar_new(cursor, cursor->fetchArraySize, transformNum,
                    size, 0, objectType);
            if (!var) {
                Py_XDECREF(objectType);
                return -1;
            }
        }

        // add the variable to the fetch variables and perform define
        Py_XDECREF(objectType);
        PyList_SET_ITEM(cursor->fetchVariables, pos - 1, (PyObject *) var);
        if (dpiStmt_define(cursor->handle, pos, var->handle) < 0)
            return cxoError_raiseAndReturnInt();

    }

    return 0;
}


//-----------------------------------------------------------------------------
// cxoCursor_verifyFetch()
//   Verify that fetching may happen from this cursor.
//-----------------------------------------------------------------------------
static int cxoCursor_verifyFetch(cxoCursor *cursor)
{
    uint32_t numQueryColumns;

    // make sure the cursor is open
    if (cxoCursor_isOpen(cursor) < 0)
        return -1;

    // fixup REF cursor, if applicable
    if (cursor->fixupRefCursor) {
        cursor->fetchArraySize = cursor->arraySize;
        if (dpiStmt_setFetchArraySize(cursor->handle,
                cursor->fetchArraySize) < 0)
            return cxoError_raiseAndReturnInt();
        if (dpiStmt_getNumQueryColumns(cursor->handle, &numQueryColumns) < 0)
            return cxoError_raiseAndReturnInt();
        if (cxoCursor_performDefine(cursor, numQueryColumns) < 0)
            return cxoError_raiseAndReturnInt();
        cursor->fixupRefCursor = 0;
    }

    // make sure the cursor is for a query
    if (!cursor->fetchVariables) {
        cxoError_raiseFromString(cxoInterfaceErrorException, "not a query");
        return -1;
    }

    return 0;
}


//-----------------------------------------------------------------------------
// cxoCursor_itemDescription()
//   Return a tuple describing the item at the given position.
//-----------------------------------------------------------------------------
static PyObject *cxoCursor_itemDescription(cxoCursor *cursor, uint32_t pos)
{
    int displaySize, index;
    dpiQueryInfo queryInfo;
    PyObject *tuple, *temp;
    cxoDbType *dbType;

    // get information about the column position
    if (dpiStmt_getQueryInfo(cursor->handle, pos, &queryInfo) < 0)
        return NULL;
    dbType = cxoDbType_fromDataTypeInfo(&queryInfo.typeInfo);
    if (!dbType)
        return NULL;

    // set display size based on data type
    switch (queryInfo.typeInfo.oracleTypeNum) {
        case DPI_ORACLE_TYPE_VARCHAR:
        case DPI_ORACLE_TYPE_NVARCHAR:
        case DPI_ORACLE_TYPE_CHAR:
        case DPI_ORACLE_TYPE_NCHAR:
        case DPI_ORACLE_TYPE_ROWID:
            displaySize = (int) queryInfo.typeInfo.sizeInChars;
            break;
        case DPI_ORACLE_TYPE_RAW:
            displaySize = (int) queryInfo.typeInfo.clientSizeInBytes;
            break;
        case DPI_ORACLE_TYPE_NATIVE_FLOAT:
        case DPI_ORACLE_TYPE_NATIVE_DOUBLE:
        case DPI_ORACLE_TYPE_NATIVE_INT:
        case DPI_ORACLE_TYPE_NUMBER:
            if (queryInfo.typeInfo.precision) {
                displaySize = queryInfo.typeInfo.precision + 1;
                if (queryInfo.typeInfo.scale > 0)
                    displaySize += queryInfo.typeInfo.scale + 1;
            }
            else displaySize = 127;
            break;
        case DPI_ORACLE_TYPE_DATE:
        case DPI_ORACLE_TYPE_TIMESTAMP:
            displaySize = 23;
            break;
        default:
            displaySize = 0;
    }

    // create the tuple and populate it
    tuple = PyTuple_New(7);
    if (!tuple)
        return NULL;

    // set each of the items in the tuple
    PyTuple_SET_ITEM(tuple, 0, PyUnicode_Decode(queryInfo.name,
            queryInfo.nameLength, cursor->connection->encodingInfo.encoding,
            NULL));
    Py_INCREF(dbType);
    PyTuple_SET_ITEM(tuple, 1, (PyObject*) dbType);
    if (displaySize)
        PyTuple_SET_ITEM(tuple, 2, PyLong_FromLong(displaySize));
    else {
        Py_INCREF(Py_None);
        PyTuple_SET_ITEM(tuple, 2, Py_None);
    }
    if (queryInfo.typeInfo.clientSizeInBytes)
        PyTuple_SET_ITEM(tuple, 3,
                PyLong_FromLong(queryInfo.typeInfo.clientSizeInBytes));
    else {
        Py_INCREF(Py_None);
        PyTuple_SET_ITEM(tuple, 3, Py_None);
    }
    if (queryInfo.typeInfo.precision || queryInfo.typeInfo.scale ||
            queryInfo.typeInfo.fsPrecision) {
        PyTuple_SET_ITEM(tuple, 4,
                PyLong_FromLong(queryInfo.typeInfo.precision));
        PyTuple_SET_ITEM(tuple, 5,
                PyLong_FromLong(queryInfo.typeInfo.scale +
                        queryInfo.typeInfo.fsPrecision));
    } else {
        Py_INCREF(Py_None);
        PyTuple_SET_ITEM(tuple, 4, Py_None);
        Py_INCREF(Py_None);
        PyTuple_SET_ITEM(tuple, 5, Py_None);
    }
    PyTuple_SET_ITEM(tuple, 6, PyLong_FromLong(queryInfo.nullOk != 0));

    // make sure the tuple is ok
    for (index = 0; index < 7; index++) {
        temp = PyTuple_GET_ITEM(tuple, index);
        if (!temp) {
            Py_DECREF(tuple);
            return NULL;
        } else if (temp == Py_None)
            Py_INCREF(temp);
    }

    return tuple;
}


//-----------------------------------------------------------------------------
// cxoCursor_getDescription()
//   Return a list of 7-tuples consisting of the description of the define
// variables.
//-----------------------------------------------------------------------------
static PyObject *cxoCursor_getDescription(cxoCursor *cursor, void *unused)
{
    uint32_t numQueryColumns, i;
    PyObject *results, *tuple;

    // make sure the cursor is open
    if (cxoCursor_isOpen(cursor) < 0)
        return NULL;

    // determine the number of query columns; if not a query return None
    if (!cursor->handle)
        Py_RETURN_NONE;
    if (dpiStmt_getNumQueryColumns(cursor->handle, &numQueryColumns) < 0)
        return cxoError_raiseAndReturnNull();
    if (numQueryColumns == 0)
        Py_RETURN_NONE;

    // create a list of the required length
    results = PyList_New(numQueryColumns);
    if (!results)
        return NULL;

    // create tuples corresponding to the select-items
    for (i = 0; i < numQueryColumns; i++) {
        tuple = cxoCursor_itemDescription(cursor, i + 1);
        if (!tuple) {
            Py_DECREF(results);
            return NULL;
        }
        PyList_SET_ITEM(results, i, tuple);
    }

    return results;
}


//-----------------------------------------------------------------------------
// cxoCursor_getLastRowid()
//   Return the rowid of the last modified row if applicable. If no row was
// modified the value None is returned.
//-----------------------------------------------------------------------------
static PyObject *cxoCursor_getLastRowid(cxoCursor *cursor, void *unused)
{
    uint32_t rowidStrLength;
    const char *rowidStr;
    dpiRowid *rowid;

    // make sure the cursor is open
    if (cxoCursor_isOpen(cursor) < 0)
        return NULL;

    // get the value, if applicable
    if (cursor->handle) {
        if (dpiStmt_getLastRowid(cursor->handle, &rowid) < 0)
            return cxoError_raiseAndReturnNull();
        if (rowid) {
            if (dpiRowid_getStringValue(rowid, &rowidStr, &rowidStrLength) < 0)
                return cxoError_raiseAndReturnNull();
            return PyUnicode_Decode(rowidStr, rowidStrLength,
                    cursor->connection->encodingInfo.encoding, NULL);
        }
    }

    Py_RETURN_NONE;
}


//-----------------------------------------------------------------------------
// cxoCursor_getOciAttr()
//   Return the value of the OCI attribute. This is intended to be used for
// testing attributes which are not currently exposed directly and should only
// be used for that purpose.
//-----------------------------------------------------------------------------
static PyObject *cxoCursor_getOciAttr(cxoCursor *cursor, PyObject *args,
        PyObject *keywordArgs)
{
    static char *keywordList[] = { "attr_num", "attr_type", NULL };
    unsigned attrNum, attrType;
    uint32_t valueLength;
    dpiDataBuffer value;

    // validate parameters
    if (!PyArg_ParseTupleAndKeywords(args, keywordArgs, "II", keywordList,
            &attrNum, &attrType))
        return NULL;
    if (cxoCursor_isOpen(cursor) < 0)
        return NULL;

    // get value and convert it to the appropriate Python value
    if (dpiStmt_getOciAttr(cursor->handle, attrNum, &value, &valueLength) < 0)
        return cxoError_raiseAndReturnNull();
    return cxoUtils_convertOciAttrToPythonValue(attrType, &value, valueLength,
            cursor->connection->encodingInfo.encoding);
}


//-----------------------------------------------------------------------------
// cxoCursor_getPrefetchRows()
//   Return an integer providing the number of rows that are prefetched by the
// Oracle Client library.
//-----------------------------------------------------------------------------
static PyObject *cxoCursor_getPrefetchRows(cxoCursor *cursor, void *unused)
{
    if (cxoCursor_isOpen(cursor) < 0)
        return NULL;
    return PyLong_FromUnsignedLong(cursor->prefetchRows);
}


//-----------------------------------------------------------------------------
// cxoCursor_close()
//   Close the cursor. Any action taken on this cursor from this point forward
// results in an exception being raised.
//-----------------------------------------------------------------------------
static PyObject *cxoCursor_close(cxoCursor *cursor, PyObject *args)
{
    if (cxoCursor_isOpen(cursor) < 0)
        return NULL;
    Py_CLEAR(cursor->bindVariables);
    Py_CLEAR(cursor->fetchVariables);
    if (cursor->handle) {
        if (dpiStmt_close(cursor->handle, NULL, 0) < 0)
            return cxoError_raiseAndReturnNull();
        dpiStmt_release(cursor->handle);
        cursor->handle = NULL;
    }
    cursor->isOpen = 0;

    Py_RETURN_NONE;
}


//-----------------------------------------------------------------------------
// cxoCursor_setBindVariableHelper()
//   Helper for setting a bind variable.
//-----------------------------------------------------------------------------
static int cxoCursor_setBindVariableHelper(cxoCursor *cursor,
        unsigned numElements, unsigned arrayPos, PyObject *value,
        cxoVar *origVar, cxoVar **newVar, int deferTypeAssignment)
{
    cxoVar *varToSet;
    int isValueVar;

    // initialization
    *newVar = NULL;
    isValueVar = cxoVar_check(value);

    // handle case where variable is already bound, either from a prior
    // execution or a call to setinputsizes()
    if (origVar) {

        // if the value is a variable object, rebind it if necessary
        if (isValueVar) {
            if ( (PyObject*) origVar != value) {
                Py_INCREF(value);
                *newVar = (cxoVar*) value;
            }

        // otherwise, attempt to set the value, but if this fails, simply
        // ignore the original bind variable and create a new one; this is
        // intended for cases where the type changes between executions of a
        // statement or where setinputsizes() has been called with the wrong
        // type (as mandated by the DB API)
        } else {
            varToSet = origVar;

            // first check to see if the variable transform is for None (which
            // can happen if all of the values in a previous invocation of
            // executemany() were None) and there is now a value; in this case,
            // discard the original variable and have a new one created
            if (origVar->transformNum == CXO_TRANSFORM_NONE &&
                    value != Py_None) {
                origVar = NULL;
                varToSet = NULL;

            // otherwise, if the number of elements has changed, create a new
            // variable this is only necessary for executemany() since
            // execute() always passes a value of 1 for the number of elements
            } else if (numElements > origVar->allocatedElements) {
                *newVar = cxoVar_new(cursor, numElements,
                        origVar->transformNum, origVar->size, origVar->isArray,
                        origVar->objectType);
                if (!*newVar)
                    return -1;
                varToSet = *newVar;
            }

            // attempt to set the value
            if (varToSet && cxoVar_setValue(varToSet, arrayPos, value) < 0) {

                // executemany() should simply fail after the first element
                if (arrayPos > 0)
                    return -1;

                // clear the exception and try to create a new variable
                PyErr_Clear();
                Py_CLEAR(*newVar);
                origVar = NULL;

            }

        }

    }

    // if no original variable used, create a new one
    if (!origVar) {

        // if the value is a variable object, bind it directly
        if (isValueVar) {
            Py_INCREF(value);
            *newVar = (cxoVar*) value;

        // otherwise, create a new variable, unless the value is None and
        // we wish to defer type assignment
        } else if (value != Py_None || !deferTypeAssignment) {
            *newVar = cxoVar_newByValue(cursor, value, numElements);
            if (!*newVar)
                return -1;
            if (cxoVar_setValue(*newVar, arrayPos, value) < 0) {
                Py_CLEAR(*newVar);
                return -1;
            }
        }

    }

    return 0;
}


//-----------------------------------------------------------------------------
// cxoCursor_setBindVariables()
//   Create or set bind variables.
//-----------------------------------------------------------------------------
int cxoCursor_setBindVariables(cxoCursor *cursor, PyObject *parameters,
        unsigned numElements, unsigned arrayPos, int deferTypeAssignment)
{
    uint32_t i, origBoundByPos, origNumParams, boundByPos, numParams;
    PyObject *key, *value, *origVar;
    cxoVar *newVar;
    Py_ssize_t pos, temp;

    // make sure positional and named binds are not being intermixed
    origNumParams = numParams = 0;
    boundByPos = PySequence_Check(parameters);
    if (boundByPos) {
        temp = PySequence_Size(parameters);
        if (temp < 0)
            return -1;
        numParams = (uint32_t) temp;
    }
    if (cursor->bindVariables) {
        origBoundByPos = PyList_Check(cursor->bindVariables);
        if (boundByPos != origBoundByPos) {
            cxoError_raiseFromString(cxoProgrammingErrorException,
                    "positional and named binds cannot be intermixed");
            return -1;
        }
        if (origBoundByPos)
            origNumParams = (uint32_t) PyList_GET_SIZE(cursor->bindVariables);

    // otherwise, create the list or dictionary if needed
    } else {
        if (boundByPos)
            cursor->bindVariables = PyList_New(numParams);
        else cursor->bindVariables = PyDict_New();
        if (!cursor->bindVariables)
            return -1;
    }

    // handle positional binds
    if (boundByPos) {
        for (i = 0; i < numParams; i++) {
            value = PySequence_GetItem(parameters, i);
            if (!value)
                return -1;
            Py_DECREF(value);
            if (i < origNumParams) {
                origVar = PyList_GET_ITEM(cursor->bindVariables, i);
                if (origVar == Py_None)
                    origVar = NULL;
            } else origVar = NULL;
            if (cxoCursor_setBindVariableHelper(cursor, numElements, arrayPos,
                    value, (cxoVar*) origVar, &newVar,
                    deferTypeAssignment) < 0)
                return -1;
            if (newVar) {
                if (i < (uint32_t) PyList_GET_SIZE(cursor->bindVariables)) {
                    if (PyList_SetItem(cursor->bindVariables, i,
                            (PyObject*) newVar) < 0) {
                        Py_DECREF(newVar);
                        return -1;
                    }
                } else {
                    if (PyList_Append(cursor->bindVariables,
                            (PyObject*) newVar) < 0) {
                        Py_DECREF(newVar);
                        return -1;
                    }
                    Py_DECREF(newVar);
                }
            }
        }

    // handle named binds
    } else {
        pos = 0;
        while (PyDict_Next(parameters, &pos, &key, &value)) {
            origVar = PyDict_GetItem(cursor->bindVariables, key);
            if (cxoCursor_setBindVariableHelper(cursor, numElements, arrayPos,
                    value, (cxoVar*) origVar, &newVar,
                    deferTypeAssignment) < 0)
                return -1;
            if (newVar) {
                if (PyDict_SetItem(cursor->bindVariables, key,
                        (PyObject*) newVar) < 0) {
                    Py_DECREF(newVar);
                    return -1;
                }
                Py_DECREF(newVar);
            }
        }
    }

    return 0;
}


//-----------------------------------------------------------------------------
// cxoCursor_performBind()
//   Perform the binds on the cursor.
//-----------------------------------------------------------------------------
int cxoCursor_performBind(cxoCursor *cursor)
{
    PyObject *key, *var;
    Py_ssize_t pos;
    int i;

    // ensure that input sizes are reset
    // this is done before binding is attempted so that if binding fails and
    // a new statement is prepared, the bind variables will be reset and
    // spurious errors will not occur
    cursor->setInputSizes = 0;

    // set values and perform binds for all bind variables
    if (cursor->bindVariables) {
        if (PyDict_Check(cursor->bindVariables)) {
            pos = 0;
            while (PyDict_Next(cursor->bindVariables, &pos, &key, &var)) {
                if (cxoVar_bind((cxoVar*) var, cursor, key, 0) < 0)
                    return -1;
            }
        } else {
            for (i = 0; i < PyList_GET_SIZE(cursor->bindVariables); i++) {
                var = PyList_GET_ITEM(cursor->bindVariables, i);
                if (var != Py_None) {
                    if (cxoVar_bind((cxoVar*) var, cursor, NULL,
                            i + 1) < 0)
                        return -1;
                }
            }
        }
    }

    return 0;
}


//-----------------------------------------------------------------------------
// cxoCursor_createRow()
//   Create an object for the row. The object created is a tuple unless a row
// factory function has been defined in which case it is the result of the
// row factory function called with the argument tuple that would otherwise be
// returned.
//-----------------------------------------------------------------------------
static PyObject *cxoCursor_createRow(cxoCursor *cursor, uint32_t pos)
{
    PyObject *tuple, *item, *result;
    Py_ssize_t numItems, i;
    cxoVar *var;

    // bump row count as a new row has been found
    cursor->rowCount++;

    // create a new tuple
    numItems = PyList_GET_SIZE(cursor->fetchVariables);
    tuple = PyTuple_New(numItems);
    if (!tuple)
        return NULL;

    // acquire the value for each item
    for (i = 0; i < numItems; i++) {
        var = (cxoVar*) PyList_GET_ITEM(cursor->fetchVariables, i);
        item = cxoVar_getSingleValue(var, var->data, pos);
        if (!item) {
            Py_DECREF(tuple);
            return NULL;
        }
        PyTuple_SET_ITEM(tuple, i, item);
    }

    // if a row factory is defined, call it
    if (cursor->rowFactory && cursor->rowFactory != Py_None) {
        result = PyObject_CallObject(cursor->rowFactory, tuple);
        Py_DECREF(tuple);
        return result;
    }

    return tuple;
}


//-----------------------------------------------------------------------------
// cxoCursor_internalPrepare()
//   Internal method for preparing a statement for execution.
//-----------------------------------------------------------------------------
static int cxoCursor_internalPrepare(cxoCursor *cursor, PyObject *statement,
        PyObject *statementTag)
{
    cxoBuffer statementBuffer, tagBuffer;
    int status;

    // make sure we don't get a situation where nothing is to be executed
    if (statement == Py_None && !cursor->statement) {
        cxoError_raiseFromString(cxoProgrammingErrorException,
                "no statement specified and no prior statement prepared");
        return -1;
    }

    // nothing to do if the statement is identical to the one already stored
    // but go ahead and prepare anyway for create, alter and drop statments
    if (statement == Py_None || statement == cursor->statement) {
        if (cursor->handle && !cursor->stmtInfo.isDDL)
            return 0;
        statement = cursor->statement;
    }

    // keep track of the statement
    Py_XDECREF(cursor->statement);
    Py_INCREF(statement);
    cursor->statement = statement;

    // keep track of the tag
    Py_XDECREF(cursor->statementTag);
    Py_XINCREF(statementTag);
    cursor->statementTag = statementTag;

    // clear fetch and bind variables if applicable
    Py_CLEAR(cursor->fetchVariables);
    if (!cursor->setInputSizes)
        Py_CLEAR(cursor->bindVariables);

    // prepare statement
    if (cxoBuffer_fromObject(&statementBuffer, statement,
            cursor->connection->encodingInfo.encoding) < 0)
        return -1;
    if (cxoBuffer_fromObject(&tagBuffer, statementTag,
            cursor->connection->encodingInfo.encoding) < 0) {
        cxoBuffer_clear(&statementBuffer);
        return -1;
    }
    Py_BEGIN_ALLOW_THREADS
    if (cursor->handle)
        dpiStmt_release(cursor->handle);
    status = dpiConn_prepareStmt(cursor->connection->handle,
            cursor->isScrollable, (const char*) statementBuffer.ptr,
            statementBuffer.size, (const char*) tagBuffer.ptr, tagBuffer.size,
            &cursor->handle);
    Py_END_ALLOW_THREADS
    cxoBuffer_clear(&statementBuffer);
    cxoBuffer_clear(&tagBuffer);
    if (status < 0)
        return cxoError_raiseAndReturnInt();

    // get statement information
    if (dpiStmt_getInfo(cursor->handle, &cursor->stmtInfo) < 0)
        return cxoError_raiseAndReturnInt();

    // set the fetch array size, if applicable
    if (cursor->stmtInfo.statementType == DPI_STMT_TYPE_SELECT) {
        if (dpiStmt_setFetchArraySize(cursor->handle, cursor->arraySize) < 0)
            return cxoError_raiseAndReturnInt();
    }

    // set number of rows to prefetch on execute, if applicable
    if (cursor->prefetchRows != DPI_DEFAULT_PREFETCH_ROWS) {
        if (dpiStmt_setPrefetchRows(cursor->handle, cursor->prefetchRows) < 0)
            return cxoError_raiseAndReturnInt();
    }

    // clear row factory, if applicable
    Py_CLEAR(cursor->rowFactory);

    return 0;
}


//-----------------------------------------------------------------------------
// cxoCursor_parse()
//   Parse the statement without executing it. This also retrieves information
// about the select list for select statements.
//-----------------------------------------------------------------------------
static PyObject *cxoCursor_parse(cxoCursor *cursor, PyObject *statement)
{
    uint32_t mode, numQueryColumns;
    dpiStmtInfo stmtInfo;
    int status;

    // make sure the cursor is open
    if (cxoCursor_isOpen(cursor) < 0)
        return NULL;

    // prepare the statement and get statement information
    if (cxoCursor_internalPrepare(cursor, statement, NULL) < 0)
        return NULL;
    if (dpiStmt_getInfo(cursor->handle, &stmtInfo) < 0)
        return cxoError_raiseAndReturnNull();

    // parse the statement
    if (stmtInfo.isQuery)
        mode = DPI_MODE_EXEC_DESCRIBE_ONLY;
    else mode = DPI_MODE_EXEC_PARSE_ONLY;
    Py_BEGIN_ALLOW_THREADS
    status = dpiStmt_execute(cursor->handle, mode, &numQueryColumns);
    Py_END_ALLOW_THREADS
    if (status < 0)
        return cxoError_raiseAndReturnNull();

    Py_RETURN_NONE;
}


//-----------------------------------------------------------------------------
// cxoCursor_prepare()
//   Prepare the statement for execution.
//-----------------------------------------------------------------------------
static PyObject *cxoCursor_prepare(cxoCursor *cursor, PyObject *args)
{
    PyObject *statement, *statementTag;

    // statement text and optional tag is expected
    statementTag = NULL;
    if (!PyArg_ParseTuple(args, "O|O", &statement, &statementTag))
        return NULL;

    // make sure the cursor is open
    if (cxoCursor_isOpen(cursor) < 0)
        return NULL;

    // prepare the statement
    if (cxoCursor_internalPrepare(cursor, statement, statementTag) < 0)
        return NULL;

    Py_RETURN_NONE;
}


//-----------------------------------------------------------------------------
// cxoCursor_callCalculateSize()
//   Calculate the size of the statement that is to be executed.
//-----------------------------------------------------------------------------
static int cxoCursor_callCalculateSize(PyObject *name,
        cxoVar *returnValue, PyObject *listOfArguments,
        PyObject *keywordArguments, int *size)
{
    Py_ssize_t numPositionalArgs, numKeywordArgs;

    // set base size without any arguments
    *size = 17;

    // add any additional space required to handle the return value
    if (returnValue)
        *size += 6;

    // assume up to 9 characters for each positional argument
    // this allows up to four digits for the placeholder if the bind variale
    // is a boolean value (prior to Oracle 12.1)
    numPositionalArgs = 0;
    if (listOfArguments) {
        numPositionalArgs = PySequence_Size(listOfArguments);
        if (numPositionalArgs < 0)
            return -1;
        *size += (int) (numPositionalArgs * 9);
    }

    // assume up to 15 characters for each keyword argument
    // this allows up to four digits for the placeholder if the bind variable
    // is a boolean value (prior to Oracle 12.1)
    numKeywordArgs = 0;
    if (keywordArguments) {
        numKeywordArgs = PyDict_Size(keywordArguments);
        if (numKeywordArgs < 0)
            return -1;
        *size += (int) (numKeywordArgs * 15);
    }

    // the above assume a maximum of 10,000 arguments; check and raise an
    // error if the number of arguments exceeds this value; more than this
    // number would probably be unusable in any case!
    if (numPositionalArgs + numKeywordArgs > 10000) {
        cxoError_raiseFromString(cxoInterfaceErrorException,
                "too many arguments");
        return -1;
    }

    return 0;
}


//-----------------------------------------------------------------------------
// cxoCursor_callBuildStatement()
//   Determine the statement and the bind variables to bind to the statement
// that is created for calling a stored procedure or function.
//-----------------------------------------------------------------------------
static int cxoCursor_callBuildStatement(PyObject *name,
        cxoVar *returnValue, PyObject *listOfArguments,
        PyObject *keywordArguments, char *statement, PyObject **statementObj,
        PyObject **bindVariables)
{
    PyObject *key, *value, *formatArgs, *positionalArgs;
    uint32_t i, argNum, numPositionalArgs;
    Py_ssize_t pos;
    char *ptr;

    // initialize the bind variables to the list of positional arguments
    if (listOfArguments)
        *bindVariables = PySequence_List(listOfArguments);
    else *bindVariables = PyList_New(0);
    if (!*bindVariables)
        return -1;

    // insert the return variable, if applicable
    if (returnValue) {
        if (PyList_Insert(*bindVariables, 0, (PyObject*) returnValue) < 0)
            return -1;
    }

    // initialize format arguments
    formatArgs = PyList_New(0);
    if (!formatArgs)
        return -1;
    if (PyList_Append(formatArgs, name) < 0) {
        Py_DECREF(formatArgs);
        return -1;
    }

    // begin building the statement
    argNum = 1;
    strcpy(statement, "begin ");
    if (returnValue) {
        strcat(statement, ":1 := ");
        argNum++;
    }
    strcat(statement, "%s");
    ptr = statement + strlen(statement);
    *ptr++ = '(';

    // include any positional arguments first
    if (listOfArguments) {
        positionalArgs = PySequence_Fast(listOfArguments,
                "expecting sequence of arguments");
        if (!positionalArgs) {
            Py_DECREF(formatArgs);
            return -1;
        }
        numPositionalArgs = (uint32_t) PySequence_Size(listOfArguments);
        for (i = 0; i < numPositionalArgs; i++) {
            if (i > 0)
                *ptr++ = ',';
            ptr += sprintf(ptr, ":%d", argNum++);
            if (cxoClientVersionInfo.versionNum < 12 &&
                    PyBool_Check(PySequence_Fast_GET_ITEM(positionalArgs, i)))
                ptr += sprintf(ptr, " = 1");
        }
        Py_DECREF(positionalArgs);
    }

    // next append any keyword arguments
    if (keywordArguments) {
        pos = 0;
        while (PyDict_Next(keywordArguments, &pos, &key, &value)) {
            if (PyList_Append(*bindVariables, value) < 0) {
                Py_DECREF(formatArgs);
                return -1;
            }
            if (PyList_Append(formatArgs, key) < 0) {
                Py_DECREF(formatArgs);
                return -1;
            }
            if ((argNum > 1 && !returnValue) || (argNum > 2 && returnValue))
                *ptr++ = ',';
            ptr += sprintf(ptr, "%%s => :%d", argNum++);
            if (cxoClientVersionInfo.versionNum < 12 &&
                    PyBool_Check(value))
                ptr += sprintf(ptr, " = 1");
        }
    }

    // create statement object
    strcpy(ptr, "); end;");
    *statementObj = cxoUtils_formatString(statement,
            PyList_AsTuple(formatArgs));
    Py_DECREF(formatArgs);
    if (!*statementObj)
        return -1;

    return 0;
}


//-----------------------------------------------------------------------------
// cxoCursor_call()
//   Call a stored procedure or function.
//-----------------------------------------------------------------------------
static int cxoCursor_call(cxoCursor *cursor, cxoVar *returnValue,
        PyObject *name, PyObject *listOfArguments, PyObject *keywordArguments)
{
    PyObject *bindVariables, *statementObj, *results;
    int statementSize;
    char *statement;

    // verify that the arguments are passed correctly
    if (listOfArguments) {
        if (!PySequence_Check(listOfArguments)) {
            PyErr_SetString(PyExc_TypeError, "arguments must be a sequence");
            return -1;
        }
    }
    if (keywordArguments) {
        if (!PyDict_Check(keywordArguments)) {
            PyErr_SetString(PyExc_TypeError,
                    "keyword arguments must be a dictionary");
            return -1;
        }
    }

    // make sure the cursor is open
    if (cxoCursor_isOpen(cursor) < 0)
        return -1;

    // determine the statement size
    if (cxoCursor_callCalculateSize(name, returnValue, listOfArguments,
            keywordArguments, &statementSize) < 0)
        return -1;

    // allocate a string for the statement
    statement = (char*) PyMem_Malloc(statementSize);
    if (!statement) {
        PyErr_NoMemory();
        return -1;
    }

    // determine the statement to execute and the argument to pass
    bindVariables = statementObj = NULL;
    if (cxoCursor_callBuildStatement(name, returnValue, listOfArguments,
            keywordArguments, statement, &statementObj, &bindVariables) < 0) {
        PyMem_Free(statement);
        Py_XDECREF(statementObj);
        Py_XDECREF(bindVariables);
        return -1;
    }
    PyMem_Free(statement);

    // execute the statement on the cursor
    results = PyObject_CallMethod( (PyObject*) cursor, "execute", "OO",
            statementObj, bindVariables);
    Py_DECREF(statementObj);
    Py_DECREF(bindVariables);
    if (!results)
        return -1;
    Py_DECREF(results);

    return 0;
}


//-----------------------------------------------------------------------------
// cxoCursor_callFunc()
//   Call a stored function and return the return value of the function.
//-----------------------------------------------------------------------------
static PyObject *cxoCursor_callFunc(cxoCursor *cursor, PyObject *args,
        PyObject *keywordArgs)
{
    static char *keywordList[] = { "name", "returnType", "parameters",
            "keyword_parameters", "keywordParameters", NULL };
    PyObject *listOfArguments, *keywordArguments, *keywordArgumentsDeprecated;
    PyObject *returnType, *results, *name;
    cxoVar *var;

    // parse arguments
    listOfArguments = keywordArguments = keywordArgumentsDeprecated = NULL;
    if (!PyArg_ParseTupleAndKeywords(args, keywordArgs, "OO|OOO", keywordList,
            &name, &returnType, &listOfArguments, &keywordArguments,
            &keywordArgumentsDeprecated))
        return NULL;
    if (keywordArgumentsDeprecated) {
        if (keywordArguments) {
            cxoError_raiseFromString(cxoProgrammingErrorException,
                    "keyword_parameters and keywordParameters cannot both be "
                    "specified");
            return NULL;
        }
        keywordArguments = keywordArgumentsDeprecated;
    }

    // create the return variable
    var = cxoVar_newByType(cursor, returnType, 1);
    if (!var)
        return NULL;

    // call the function
    if (cxoCursor_call(cursor, var, name, listOfArguments,
            keywordArguments) < 0)
        return NULL;

    // determine the results
    results = cxoVar_getValue(var, 0);
    Py_DECREF(var);
    return results;
}


//-----------------------------------------------------------------------------
// cxoCursor_callProc()
//   Call a stored procedure and return the (possibly modified) arguments.
//-----------------------------------------------------------------------------
static PyObject *cxoCursor_callProc(cxoCursor *cursor, PyObject *args,
        PyObject *keywordArgs)
{
    static char *keywordList[] = { "name", "parameters",
            "keyword_parameters", "keywordParameters", NULL };
    PyObject *listOfArguments, *keywordArguments, *keywordArgumentsDeprecated;
    PyObject *results, *var, *temp, *name;
    Py_ssize_t numArgs, i;

    // parse arguments
    listOfArguments = keywordArguments = keywordArgumentsDeprecated = NULL;
    if (!PyArg_ParseTupleAndKeywords(args, keywordArgs, "O|OOO", keywordList,
            &name, &listOfArguments, &keywordArguments,
            &keywordArgumentsDeprecated))
        return NULL;
    if (keywordArgumentsDeprecated) {
        if (keywordArguments) {
            cxoError_raiseFromString(cxoProgrammingErrorException,
                    "keyword_parameters and keywordParameters cannot both be "
                    "specified");
            return NULL;
        }
        keywordArguments = keywordArgumentsDeprecated;
    }

    // call the stored procedure
    if (cxoCursor_call(cursor, NULL, name, listOfArguments,
            keywordArguments) < 0)
        return NULL;

    // create the return value (only positional arguments are returned)
    numArgs = (listOfArguments) ? PySequence_Size(listOfArguments) : 0;
    results = PyList_New(numArgs);
    if (!results)
        return NULL;
    for (i = 0; i < numArgs; i++) {
        var = PyList_GET_ITEM(cursor->bindVariables, i);
        temp = cxoVar_getValue((cxoVar*) var, 0);
        if (!temp) {
            Py_DECREF(results);
            return NULL;
        }
        PyList_SET_ITEM(results, i, temp);
    }

    return results;
}


//-----------------------------------------------------------------------------
// cxoCursor_execute()
//   Execute the statement.
//-----------------------------------------------------------------------------
static PyObject *cxoCursor_execute(cxoCursor *cursor, PyObject *args,
        PyObject *keywordArgs)
{
    PyObject *statement, *executeArgs;
    uint32_t numQueryColumns, mode;
    int status;

    executeArgs = NULL;
    if (!PyArg_ParseTuple(args, "O|O", &statement, &executeArgs))
        return NULL;
    if (executeArgs && keywordArgs) {
        if (PyDict_Size(keywordArgs) == 0)
            keywordArgs = NULL;
        else return cxoError_raiseFromString(cxoInterfaceErrorException,
                "expecting argument or keyword arguments, not both");
    }
    if (keywordArgs)
        executeArgs = keywordArgs;
    if (executeArgs) {
        if (!PyDict_Check(executeArgs) && !PySequence_Check(executeArgs)) {
            PyErr_SetString(PyExc_TypeError,
                    "expecting a dictionary, sequence or keyword args");
            return NULL;
        }
    }

    // make sure the cursor is open
    if (cxoCursor_isOpen(cursor) < 0)
        return NULL;

    // prepare the statement, if applicable
    if (cxoCursor_internalPrepare(cursor, statement, NULL) < 0)
        return NULL;

    // perform binds
    if (executeArgs && cxoCursor_setBindVariables(cursor, executeArgs, 1, 0,
            0) < 0)
        return NULL;
    if (cxoCursor_performBind(cursor) < 0)
        return NULL;

    // execute the statement
    Py_BEGIN_ALLOW_THREADS
    mode = (cursor->connection->autocommit) ? DPI_MODE_EXEC_COMMIT_ON_SUCCESS :
            DPI_MODE_EXEC_DEFAULT;
    status = dpiStmt_execute(cursor->handle, mode, &numQueryColumns);
    Py_END_ALLOW_THREADS
    if (status < 0)
        return cxoError_raiseAndReturnNull();

    // get the count of the rows affected
    if (dpiStmt_getRowCount(cursor->handle, &cursor->rowCount) < 0)
        return cxoError_raiseAndReturnNull();

    // for queries, return the cursor for convenience
    if (numQueryColumns > 0) {
        if (cxoCursor_performDefine(cursor, numQueryColumns) < 0) {
            Py_CLEAR(cursor->fetchVariables);
            return NULL;
        }
        Py_INCREF(cursor);
        return (PyObject*) cursor;
    }

    // for statements other than queries, simply return None
    Py_RETURN_NONE;
}


//-----------------------------------------------------------------------------
// cxoCursor_executeMany()
//   Execute the statement many times. The number of times is equivalent to the
// number of elements in the list of parameters, or the provided integer if no
// parameters are required.
//-----------------------------------------------------------------------------
static PyObject *cxoCursor_executeMany(cxoCursor *cursor, PyObject *args,
        PyObject *keywordArgs)
{
    static char *keywordList[] = { "statement", "parameters", "batcherrors",
            "arraydmlrowcounts", NULL };
    int arrayDMLRowCountsEnabled = 0, batchErrorsEnabled = 0;
    PyObject *arguments, *parameters, *statement;
    uint32_t mode, i, numRows;
    int status;

    // validate parameters
    if (!PyArg_ParseTupleAndKeywords(args, keywordArgs, "OO|ii", keywordList,
            &statement, &parameters, &batchErrorsEnabled,
            &arrayDMLRowCountsEnabled))
        return NULL;
    if (!PyList_Check(parameters) && !PyLong_Check(parameters)) {
        PyErr_SetString(PyExc_TypeError,
                "parameters should be a list of sequences/dictionaries "
                "or an integer specifying the number of times to execute "
                "the statement");
        return NULL;
    }

    // make sure the cursor is open
    if (cxoCursor_isOpen(cursor) < 0)
        return NULL;

    // determine execution mode
    mode = (cursor->connection->autocommit) ? DPI_MODE_EXEC_COMMIT_ON_SUCCESS :
            DPI_MODE_EXEC_DEFAULT;
    if (batchErrorsEnabled)
        mode |= DPI_MODE_EXEC_BATCH_ERRORS;
    if (arrayDMLRowCountsEnabled)
        mode |= DPI_MODE_EXEC_ARRAY_DML_ROWCOUNTS;

    // prepare the statement
    if (cxoCursor_internalPrepare(cursor, statement, NULL) < 0)
        return NULL;

    // perform binds, as required
    if (PyLong_Check(parameters))
        numRows = (uint32_t) PyLong_AsLong(parameters);
    else {
        numRows = (uint32_t) PyList_GET_SIZE(parameters);
        for (i = 0; i < numRows; i++) {
            arguments = PyList_GET_ITEM(parameters, i);
            if (!PyDict_Check(arguments) && !PySequence_Check(arguments))
                return cxoError_raiseFromString(cxoInterfaceErrorException,
                        "expecting a list of dictionaries or sequences");
            if (cxoCursor_setBindVariables(cursor, arguments, numRows, i,
                    (i < numRows - 1)) < 0)
                return NULL;
        }
    }
    if (cxoCursor_performBind(cursor) < 0)
        return NULL;

    // execute the statement, but only if the number of rows is greater than
    // zero since Oracle raises an error otherwise
    if (numRows > 0) {
        Py_BEGIN_ALLOW_THREADS
        status = dpiStmt_executeMany(cursor->handle, mode, numRows);
        Py_END_ALLOW_THREADS
        if (status < 0) {
            cxoError_raiseAndReturnNull();
            dpiStmt_getRowCount(cursor->handle, &cursor->rowCount);
            return NULL;
        }
        if (dpiStmt_getRowCount(cursor->handle, &cursor->rowCount) < 0)
            return cxoError_raiseAndReturnNull();
    }

    Py_RETURN_NONE;
}


//-----------------------------------------------------------------------------
// cxoCursor_executeManyPrepared()
//   Execute the prepared statement the number of times requested. At this
// point, the statement must have been already prepared and the bind variables
// must have their values set.
//-----------------------------------------------------------------------------
static PyObject *cxoCursor_executeManyPrepared(cxoCursor *cursor,
        PyObject *args)
{
    int numIters, status;

    // expect number of times to execute the statement
    if (!PyArg_ParseTuple(args, "i", &numIters))
        return NULL;

    // make sure the cursor is open
    if (cxoCursor_isOpen(cursor) < 0)
        return NULL;

    // perform binds
    if (cxoCursor_performBind(cursor) < 0)
        return NULL;

    // execute the statement
    Py_BEGIN_ALLOW_THREADS
    status = dpiStmt_executeMany(cursor->handle, DPI_MODE_EXEC_DEFAULT,
            numIters);
    Py_END_ALLOW_THREADS
    if (status < 0 || dpiStmt_getRowCount(cursor->handle,
            &cursor->rowCount) < 0)
        return cxoError_raiseAndReturnNull();

    Py_RETURN_NONE;
}


//-----------------------------------------------------------------------------
// cxoCursor_multiFetch()
//   Return a list consisting of the remaining rows up to the given row limit
// (if specified).
//-----------------------------------------------------------------------------
static PyObject *cxoCursor_multiFetch(cxoCursor *cursor, int rowLimit)
{
    uint32_t bufferRowIndex = 0;
    PyObject *results, *row;
    int found, rowNum;

    // verify fetch can be performed
    if (cxoCursor_verifyFetch(cursor) < 0)
        return NULL;

    // create an empty list
    results = PyList_New(0);
    if (!results)
        return NULL;

    // fetch as many rows as possible
    for (rowNum = 0; rowLimit == 0 || rowNum < rowLimit; rowNum++) {
        if (cxoCursor_fetchRow(cursor, &found, &bufferRowIndex) < 0) {
            Py_DECREF(results);
            return NULL;
        }
        if (!found)
            break;
        row = cxoCursor_createRow(cursor, bufferRowIndex);
        if (!row) {
            Py_DECREF(results);
            return NULL;
        }
        if (PyList_Append(results, row) < 0) {
            Py_DECREF(row);
            Py_DECREF(results);
            return NULL;
        }
        Py_DECREF(row);
    }

    return results;
}


//-----------------------------------------------------------------------------
// cxoCursor_fetchOne()
//   Fetch a single row from the cursor.
//-----------------------------------------------------------------------------
static PyObject *cxoCursor_fetchOne(cxoCursor *cursor, PyObject *args)
{
    uint32_t bufferRowIndex = 0;
    int found = 0;

    if (cxoCursor_verifyFetch(cursor) < 0)
        return NULL;
    if (cxoCursor_fetchRow(cursor, &found, &bufferRowIndex) < 0)
        return NULL;
    if (found)
        return cxoCursor_createRow(cursor, bufferRowIndex);

    Py_RETURN_NONE;
}


//-----------------------------------------------------------------------------
// cxoCursor_fetchMany()
//   Fetch multiple rows from the cursor based on the arraysize.
//-----------------------------------------------------------------------------
static PyObject *cxoCursor_fetchMany(cxoCursor *cursor, PyObject *args,
        PyObject *keywordArgs)
{
    static char *keywordList[] = { "numRows", NULL };
    int rowLimit;

    // parse arguments -- optional rowlimit expected
    rowLimit = cursor->arraySize;
    if (!PyArg_ParseTupleAndKeywords(args, keywordArgs, "|i", keywordList,
            &rowLimit))
        return NULL;

    return cxoCursor_multiFetch(cursor, rowLimit);
}


//-----------------------------------------------------------------------------
// cxoCursor_fetchAll()
//   Fetch all remaining rows from the cursor.
//-----------------------------------------------------------------------------
static PyObject *cxoCursor_fetchAll(cxoCursor *cursor, PyObject *args)
{
    return cxoCursor_multiFetch(cursor, 0);
}


//-----------------------------------------------------------------------------
// cxoCursor_fetchRaw()
//   Perform raw fetch on the cursor; return the actual number of rows fetched.
//-----------------------------------------------------------------------------
static PyObject *cxoCursor_fetchRaw(cxoCursor *cursor, PyObject *args,
        PyObject *keywordArgs)
{
    static char *keywordList[] = { "numRows", NULL };
    uint32_t numRowsToFetch, numRowsFetched, bufferRowIndex;
    int moreRows;

    // expect an optional number of rows to retrieve
    numRowsToFetch = cursor->fetchArraySize;
    if (!PyArg_ParseTupleAndKeywords(args, keywordArgs, "|i", keywordList,
            &numRowsToFetch))
        return NULL;
    if (numRowsToFetch > cursor->fetchArraySize)
        return cxoError_raiseFromString(cxoInterfaceErrorException,
                "rows to fetch exceeds array size");

    // perform the fetch
    if (dpiStmt_fetchRows(cursor->handle, numRowsToFetch, &bufferRowIndex,
            &numRowsFetched, &moreRows) < 0)
        return cxoError_raiseAndReturnNull();
    cursor->rowCount += numRowsFetched;
    cursor->numRowsInFetchBuffer = 0;
    return PyLong_FromLong(numRowsFetched);
}


//-----------------------------------------------------------------------------
// cxoCursor_scroll()
//   Scroll the cursor using the value and mode specified.
//-----------------------------------------------------------------------------
static PyObject *cxoCursor_scroll(cxoCursor *cursor, PyObject *args,
        PyObject *keywordArgs)
{
    static char *keywordList[] = { "value", "mode", NULL };
    dpiFetchMode mode;
    int32_t offset;
    char *strMode;
    int status;

    // parse arguments
    offset = 0;
    strMode = NULL;
    if (!PyArg_ParseTupleAndKeywords(args, keywordArgs, "|is", keywordList,
            &offset, &strMode))
        return NULL;

    // validate mode
    if (!strMode)
        mode = DPI_MODE_FETCH_RELATIVE;
    else if (strcmp(strMode, "relative") == 0)
        mode = DPI_MODE_FETCH_RELATIVE;
    else if (strcmp(strMode, "absolute") == 0)
        mode = DPI_MODE_FETCH_ABSOLUTE;
    else if (strcmp(strMode, "first") == 0)
        mode = DPI_MODE_FETCH_FIRST;
    else if (strcmp(strMode, "last") == 0)
        mode = DPI_MODE_FETCH_LAST;
    else return cxoError_raiseFromString(cxoInterfaceErrorException,
            "mode must be one of relative, absolute, first or last");

    // make sure the cursor is open
    if (cxoCursor_isOpen(cursor) < 0)
        return NULL;

    // perform scroll and get new row count and number of rows in buffer
    Py_BEGIN_ALLOW_THREADS
    status = dpiStmt_scroll(cursor->handle, mode, offset,
            0 - cursor->numRowsInFetchBuffer);
    if (status == 0)
        status = dpiStmt_fetchRows(cursor->handle, cursor->fetchArraySize,
                &cursor->fetchBufferRowIndex, &cursor->numRowsInFetchBuffer,
                &cursor->moreRowsToFetch);
    if (status == 0)
        status = dpiStmt_getRowCount(cursor->handle, &cursor->rowCount);
    Py_END_ALLOW_THREADS
    if (status < 0)
        return cxoError_raiseAndReturnNull();
    cursor->rowCount -= cursor->numRowsInFetchBuffer;

    Py_RETURN_NONE;
}


//-----------------------------------------------------------------------------
// cxoCursor_setInputSizes()
//   Set the sizes of the bind variables.
//-----------------------------------------------------------------------------
static PyObject *cxoCursor_setInputSizes(cxoCursor *cursor, PyObject *args,
        PyObject *keywordArgs)
{
    Py_ssize_t numPositionalArgs, numKeywordArgs = 0, i;
    PyObject *key, *value;
    cxoVar *var;

    // only expect keyword arguments or positional arguments, not both
    numPositionalArgs = PyTuple_Size(args);
    if (keywordArgs)
        numKeywordArgs = PyDict_Size(keywordArgs);
    if (numKeywordArgs > 0 && numPositionalArgs > 0)
        return cxoError_raiseFromString(cxoInterfaceErrorException,
                "expecting arguments or keyword arguments, not both");

    // make sure the cursor is open
    if (cxoCursor_isOpen(cursor) < 0)
        return NULL;

    // eliminate existing bind variables
    Py_CLEAR(cursor->bindVariables);

    // if no values passed, do nothing further, but return an empty list or
    // dictionary as appropriate
    if (numKeywordArgs == 0 && numPositionalArgs == 0) {
        if (keywordArgs)
            return PyDict_New();
        return PyList_New(0);
    }

    // retain bind variables
    cursor->setInputSizes = 1;
    if (numKeywordArgs > 0)
        cursor->bindVariables = PyDict_New();
    else cursor->bindVariables = PyList_New(numPositionalArgs);
    if (!cursor->bindVariables)
        return NULL;

    // process each input
    if (numKeywordArgs > 0) {
        i = 0;
        while (PyDict_Next(keywordArgs, &i, &key, &value)) {
            var = cxoVar_newByType(cursor, value, cursor->bindArraySize);
            if (!var)
                return NULL;
            if (PyDict_SetItem(cursor->bindVariables, key,
                    (PyObject*) var) < 0) {
                Py_DECREF(var);
                return NULL;
            }
            Py_DECREF(var);
        }
    } else {
        for (i = 0; i < numPositionalArgs; i++) {
            value = PyTuple_GET_ITEM(args, i);
            if (value == Py_None) {
                Py_INCREF(Py_None);
                PyList_SET_ITEM(cursor->bindVariables, i, Py_None);
            } else {
                var = cxoVar_newByType(cursor, value, cursor->bindArraySize);
                if (!var)
                    return NULL;
                PyList_SET_ITEM(cursor->bindVariables, i, (PyObject*) var);
            }
        }
    }

    Py_INCREF(cursor->bindVariables);
    return cursor->bindVariables;
}


//-----------------------------------------------------------------------------
// cxoCursor_setOciAttr()
//   Set the value of the OCI attribute to the specified value. This is
// intended to be used for testing attributes which are not currently exposed
// directly and should only be used for that purpose.
//-----------------------------------------------------------------------------
static PyObject *cxoCursor_setOciAttr(cxoCursor *cursor, PyObject *args,
        PyObject *keywordArgs)
{
    static char *keywordList[] = { "attr_num", "attr_type", "value", NULL };
    unsigned attrNum, attrType;
    uint32_t ociValueLength;
    dpiDataBuffer ociBuffer;
    cxoBuffer buffer;
    PyObject *value;
    void *ociValue;

    // validate parameters
    if (!PyArg_ParseTupleAndKeywords(args, keywordArgs, "IIO", keywordList,
            &attrNum, &attrType, &value))
        return NULL;
    if (cxoCursor_isOpen(cursor) < 0)
        return NULL;

    // convert value to OCI requirement and then set it
    cxoBuffer_init(&buffer);
    if (cxoUtils_convertPythonValueToOciAttr(value, attrType, &buffer,
            &ociBuffer, &ociValue, &ociValueLength,
            cursor->connection->encodingInfo.encoding) < 0)
        return NULL;
    if (dpiStmt_setOciAttr(cursor->handle, attrNum, ociValue,
            ociValueLength) < 0)
        return cxoError_raiseAndReturnNull();
    cxoBuffer_clear(&buffer);

    Py_RETURN_NONE;
}


//-----------------------------------------------------------------------------
// cxoCursor_setOutputSize()
//   Does nothing as ODPI-C handles long columns dynamically without the need
// to specify a maximum length.
//-----------------------------------------------------------------------------
static PyObject *cxoCursor_setOutputSize(cxoCursor *cursor, PyObject *args)
{
    int outputSize, outputSizeColumn;

    if (!PyArg_ParseTuple(args, "i|i", &outputSize, &outputSizeColumn))
        return NULL;
    Py_RETURN_NONE;
}


//-----------------------------------------------------------------------------
// cxoCursor_var()
//   Create a bind variable and return it.
//-----------------------------------------------------------------------------
static PyObject *cxoCursor_var(cxoCursor *cursor, PyObject *args,
        PyObject *keywordArgs)
{
    static char *keywordList[] = { "typ", "size", "arraysize", "inconverter",
            "outconverter", "typename", "encoding_errors", "bypass_decode",
            "encodingErrors", NULL };
    Py_ssize_t encodingErrorsLength, encodingErrorsDeprecatedLength;
    const char *encodingErrors, *encodingErrorsDeprecated;
    PyObject *inConverter, *outConverter, *typeNameObj;
    int size, arraySize, bypassDecode;
    cxoTransformNum transformNum;
    cxoObjectType *objType;
    PyObject *type;
    cxoVar *var;

    // parse arguments
    size = bypassDecode = 0;
    arraySize = cursor->bindArraySize;
    encodingErrors = encodingErrorsDeprecated = NULL;
    inConverter = outConverter = typeNameObj = NULL;
    if (!PyArg_ParseTupleAndKeywords(args, keywordArgs, "O|iiOOOz#pz#",
            keywordList, &type, &size, &arraySize, &inConverter, &outConverter,
            &typeNameObj, &encodingErrors, &encodingErrorsLength,
            &bypassDecode, &encodingErrorsDeprecated,
            &encodingErrorsDeprecatedLength))
        return NULL;
    if (encodingErrorsDeprecated) {
        if (encodingErrors) {
            cxoError_raiseFromString(cxoProgrammingErrorException,
                    "encoding_errors and encodingErrors cannot both be "
                    "specified");
            return NULL;
        }
        encodingErrors = encodingErrorsDeprecated;
        encodingErrorsLength = encodingErrorsDeprecatedLength;
    }

    // determine the type of variable
    if (cxoTransform_getNumFromType(type, &transformNum, &objType) < 0)
        return NULL;
    Py_XINCREF(objType);
    if (typeNameObj && typeNameObj != Py_None && !objType) {
        objType = cxoObjectType_newByName(cursor->connection, typeNameObj);
        if (!objType)
            return NULL;
    }

    // create the variable
    var = cxoVar_new(cursor, arraySize, transformNum, size, 0, objType);
    Py_XDECREF(objType);
    if (!var)
        return NULL;
    Py_XINCREF(inConverter);
    var->inConverter = inConverter;
    Py_XINCREF(outConverter);
    var->outConverter = outConverter;

    // assign encoding errors, if applicable
    if (encodingErrors) {
        var->encodingErrors = PyMem_Malloc(encodingErrorsLength + 1);
        if (!var->encodingErrors) {
            Py_DECREF(var);
            return NULL;
        }
        strcpy((char*) var->encodingErrors, encodingErrors);
    }

    // if the decode step is to be bypassed, use the binary transform instead
    if (bypassDecode)
        var->transformNum = CXO_TRANSFORM_BINARY;

    return (PyObject*) var;
}


//-----------------------------------------------------------------------------
// cxoCursor_arrayVar()
//   Create an array bind variable and return it.
//-----------------------------------------------------------------------------
static PyObject *cxoCursor_arrayVar(cxoCursor *cursor, PyObject *args)
{
    cxoTransformNum transformNum;
    uint32_t size, numElements;
    PyObject *type, *value;
    cxoObjectType *objType;
    cxoVar *var;

    // parse arguments
    size = 0;
    if (!PyArg_ParseTuple(args, "OO|i", &type, &value, &size))
        return NULL;

    // determine the transform to use for the variable
    if (cxoTransform_getNumFromType(type, &transformNum, &objType) < 0)
        return NULL;

    // determine the number of elements to create
    if (PyList_Check(value))
        numElements = (uint32_t) PyList_GET_SIZE(value);
    else if (PyLong_Check(value)) {
        numElements = (uint32_t) PyLong_AsLong(value);
        if (PyErr_Occurred())
            return NULL;
    } else {
        PyErr_SetString(PyExc_TypeError,
                "expecting integer or list of values");
        return NULL;
    }

    // create the variable
    var = cxoVar_new(cursor, numElements, transformNum, size, 1, objType);
    if (!var)
        return NULL;

    // set the value, if applicable
    if (PyList_Check(value)) {
        if (cxoVar_setValue(var, 0, value) < 0)
            return NULL;
    }

    return (PyObject*) var;
}


//-----------------------------------------------------------------------------
// cxoCursor_bindNames()
//   Return a list of bind variable names.
//-----------------------------------------------------------------------------
static PyObject *cxoCursor_bindNames(cxoCursor *cursor, PyObject *args)
{
    uint32_t numBinds, *nameLengths, i;
    PyObject *namesList, *temp;
    const char **names;

    // make sure the cursor is open
    if (cxoCursor_isOpen(cursor) < 0)
        return NULL;

    // ensure that a statement has already been prepared
    if (!cursor->statement)
        return cxoError_raiseFromString(cxoProgrammingErrorException,
                "statement must be prepared first");

    // determine the number of binds
    if (dpiStmt_getBindCount(cursor->handle, &numBinds) < 0)
        return cxoError_raiseAndReturnNull();

    // if the number of binds is zero, nothing to do
    if (numBinds == 0)
        return PyList_New(0);

    // allocate memory for the bind names and their lengths
    names = (const char**) PyMem_Malloc(numBinds * sizeof(char*));
    if (!names)
        return PyErr_NoMemory();
    nameLengths = (uint32_t*) PyMem_Malloc(numBinds * sizeof(uint32_t));
    if (!nameLengths) {
        PyMem_Free((void*) names);
        return PyErr_NoMemory();
    }

    // get the bind names
    if (dpiStmt_getBindNames(cursor->handle, &numBinds, names,
            nameLengths) < 0) {
        PyMem_Free((void*) names);
        PyMem_Free(nameLengths);
        return cxoError_raiseAndReturnNull();
    }

    // populate list with the results
    namesList = PyList_New(numBinds);
    if (namesList) {
        for (i = 0; i < numBinds; i++) {
            temp = PyUnicode_Decode(names[i], nameLengths[i],
                    cursor->connection->encodingInfo.encoding, NULL);
            if (!temp) {
                Py_CLEAR(namesList);
                break;
            }
            PyList_SET_ITEM(namesList, i, temp);
        }
    }
    PyMem_Free((void*) names);
    PyMem_Free(nameLengths);
    return namesList;
}


//-----------------------------------------------------------------------------
// cxoCursor_getIter()
//   Return a reference to the cursor which supports the iterator protocol.
//-----------------------------------------------------------------------------
static PyObject *cxoCursor_getIter(cxoCursor *cursor)
{
    if (cxoCursor_verifyFetch(cursor) < 0)
        return NULL;
    Py_INCREF(cursor);
    return (PyObject*) cursor;
}


//-----------------------------------------------------------------------------
// cxoCursor_getNext()
//   Return a reference to the cursor which supports the iterator protocol.
//-----------------------------------------------------------------------------
static PyObject *cxoCursor_getNext(cxoCursor *cursor)
{
    uint32_t bufferRowIndex = 0;
    int found = 0;

    if (cxoCursor_verifyFetch(cursor) < 0)
        return NULL;
    if (cxoCursor_fetchRow(cursor, &found, &bufferRowIndex) < 0)
        return NULL;
    if (found)
        return cxoCursor_createRow(cursor, bufferRowIndex);

    // no more rows, return NULL without setting an exception
    return NULL;
}


//-----------------------------------------------------------------------------
// cxoCursor_getBatchErrors()
//    Returns a list of batch error objects.
//-----------------------------------------------------------------------------
static PyObject* cxoCursor_getBatchErrors(cxoCursor *cursor)
{
    uint32_t numErrors, i;
    dpiErrorInfo *errors;
    PyObject *result;
    cxoError *error;

    // determine the number of errors
    if (dpiStmt_getBatchErrorCount(cursor->handle, &numErrors) < 0)
        return cxoError_raiseAndReturnNull();
    if (numErrors == 0)
        return PyList_New(0);

    // allocate memory for the errors
    errors = PyMem_Malloc(numErrors * sizeof(dpiErrorInfo));
    if (!errors)
        return PyErr_NoMemory();

    // get error information
    if (dpiStmt_getBatchErrors(cursor->handle, numErrors, errors) < 0) {
        PyMem_Free(errors);
        return cxoError_raiseAndReturnNull();
    }

    // create result
    result = PyList_New(numErrors);
    if (result) {
        for (i = 0; i < numErrors; i++) {
            error = cxoError_newFromInfo(&errors[i]);
            if (!error) {
                Py_CLEAR(result);
                break;
            }
            PyList_SET_ITEM(result, i, (PyObject*) error);
        }
    }
    PyMem_Free(errors);
    return result;
}


//-----------------------------------------------------------------------------
// cxoCursor_getArrayDMLRowCounts
//    Populates the array dml row count list.
//-----------------------------------------------------------------------------
static PyObject* cxoCursor_getArrayDMLRowCounts(cxoCursor *cursor)
{
    PyObject *result, *element;
    uint32_t numRowCounts, i;
    uint64_t *rowCounts;

    // get row counts from DPI
    if (dpiStmt_getRowCounts(cursor->handle, &numRowCounts, &rowCounts) < 0)
        return cxoError_raiseAndReturnNull();

    // return array
    result = PyList_New(numRowCounts);
    if (!result)
        return NULL;
    for (i = 0; i < numRowCounts; i++) {
        element = PyLong_FromUnsignedLong((unsigned long) rowCounts[i]);
        if (!element) {
            Py_DECREF(result);
            return NULL;
        }
        PyList_SET_ITEM(result, i, element);
    }

    return result;
}


//-----------------------------------------------------------------------------
// cxoCursor_getImplicitResults
//   Return a list of cursors available implicitly after execution of a PL/SQL
// block or stored procedure. If none are available, an empty list is returned.
//-----------------------------------------------------------------------------
static PyObject *cxoCursor_getImplicitResults(cxoCursor *cursor)
{
    cxoCursor *childCursor;
    dpiStmt *childStmt;
    PyObject *result;

    // make sure the cursor is open
    if (cxoCursor_isOpen(cursor) < 0)
        return NULL;

    // make sure we have a statement executed (handle defined)
    if (!cursor->handle)
        return cxoError_raiseFromString(cxoInterfaceErrorException,
                "no statement executed");

    // create result
    result = PyList_New(0);
    if (!result)
        return NULL;
    while (1) {
        if (dpiStmt_getImplicitResult(cursor->handle, &childStmt) < 0)
            return cxoError_raiseAndReturnNull();
        if (!childStmt)
            break;
        childCursor = (cxoCursor*) PyObject_CallMethod(
                (PyObject*) cursor->connection, "cursor", NULL);
        if (!childCursor) {
            dpiStmt_release(childStmt);
            Py_DECREF(result);
            return NULL;
        }
        childCursor->handle = childStmt;
        childCursor->fixupRefCursor = 1;
        if (PyList_Append(result, (PyObject*) childCursor) < 0) {
            Py_DECREF(result);
            Py_DECREF(childCursor);
            return NULL;
        }
        Py_DECREF(childCursor);
    }

    return result;
}


//-----------------------------------------------------------------------------
// cxoCursor_contextManagerEnter()
//   Called when the cursor is used as a context manager and simply returns it
// to the caller.
//-----------------------------------------------------------------------------
static PyObject *cxoCursor_contextManagerEnter(cxoCursor *cursor,
        PyObject* args)
{
    Py_INCREF(cursor);
    return (PyObject*) cursor;
}


//-----------------------------------------------------------------------------
// cxoCursor_contextManagerExit()
//   Called when the cursor is used as a context manager and simply closes the
// cursor.
//-----------------------------------------------------------------------------
static PyObject *cxoCursor_contextManagerExit(cxoCursor *cursor,
        PyObject* args)
{
    PyObject *excType, *excValue, *excTraceback, *result;

    if (!PyArg_ParseTuple(args, "OOO", &excType, &excValue, &excTraceback))
        return NULL;
    result = cxoCursor_close(cursor, NULL);
    if (!result)
        return NULL;
    Py_DECREF(result);
    Py_INCREF(Py_False);
    return Py_False;
}


//-----------------------------------------------------------------------------
// cxoCursor_setPrefetchRows()
//   Set the number of rows that are prefetched by the Oracle Client library.
//-----------------------------------------------------------------------------
static int cxoCursor_setPrefetchRows(cxoCursor* cursor, PyObject *value,
        void* arg)
{
    unsigned long numRows;

    if (cxoCursor_isOpen(cursor) < 0)
        return -1;
    numRows = PyLong_AsUnsignedLong(value);
    if (PyErr_Occurred())
        return -1;
    cursor->prefetchRows = (uint32_t) numRows;
    if (cursor->handle && dpiStmt_setPrefetchRows(cursor->handle,
            cursor->prefetchRows) < 0)
        return cxoError_raiseAndReturnInt();
    return 0;
}


//-----------------------------------------------------------------------------
// declaration of methods for Python type
//-----------------------------------------------------------------------------
static PyMethodDef cxoMethods[] = {
    { "execute", (PyCFunction) cxoCursor_execute,
            METH_VARARGS | METH_KEYWORDS },
    { "fetchall", (PyCFunction) cxoCursor_fetchAll, METH_NOARGS },
    { "fetchone", (PyCFunction) cxoCursor_fetchOne, METH_NOARGS },
    { "fetchmany", (PyCFunction) cxoCursor_fetchMany,
              METH_VARARGS | METH_KEYWORDS },
    { "fetchraw", (PyCFunction) cxoCursor_fetchRaw,
              METH_VARARGS | METH_KEYWORDS },
    { "prepare", (PyCFunction) cxoCursor_prepare, METH_VARARGS },
    { "parse", (PyCFunction) cxoCursor_parse, METH_O },
    { "setinputsizes", (PyCFunction) cxoCursor_setInputSizes,
              METH_VARARGS | METH_KEYWORDS },
    { "executemany", (PyCFunction) cxoCursor_executeMany,
              METH_VARARGS | METH_KEYWORDS },
    { "callproc", (PyCFunction) cxoCursor_callProc,
              METH_VARARGS  | METH_KEYWORDS },
    { "callfunc", (PyCFunction) cxoCursor_callFunc,
              METH_VARARGS  | METH_KEYWORDS },
    { "executemanyprepared", (PyCFunction) cxoCursor_executeManyPrepared,
              METH_VARARGS },
    { "setoutputsize", (PyCFunction) cxoCursor_setOutputSize, METH_VARARGS },
    { "scroll", (PyCFunction) cxoCursor_scroll, METH_VARARGS | METH_KEYWORDS },
    { "var", (PyCFunction) cxoCursor_var, METH_VARARGS | METH_KEYWORDS },
    { "arrayvar", (PyCFunction) cxoCursor_arrayVar, METH_VARARGS },
    { "bindnames", (PyCFunction) cxoCursor_bindNames, METH_NOARGS },
    { "close", (PyCFunction) cxoCursor_close, METH_NOARGS },
    { "getbatcherrors", (PyCFunction) cxoCursor_getBatchErrors, METH_NOARGS },
    { "getarraydmlrowcounts", (PyCFunction) cxoCursor_getArrayDMLRowCounts,
              METH_NOARGS },
    { "getimplicitresults", (PyCFunction) cxoCursor_getImplicitResults,
              METH_NOARGS },
    { "__enter__", (PyCFunction) cxoCursor_contextManagerEnter, METH_NOARGS },
    { "__exit__", (PyCFunction) cxoCursor_contextManagerExit, METH_VARARGS },
    { "_get_oci_attr", (PyCFunction) cxoCursor_getOciAttr,
            METH_VARARGS | METH_KEYWORDS },
    { "_set_oci_attr", (PyCFunction) cxoCursor_setOciAttr,
            METH_VARARGS | METH_KEYWORDS },
    { NULL, NULL }
};


//-----------------------------------------------------------------------------
// declaration of members for Python type
//-----------------------------------------------------------------------------
static PyMemberDef cxoMembers[] = {
    { "arraysize", T_UINT, offsetof(cxoCursor, arraySize), 0 },
    { "bindarraysize", T_UINT, offsetof(cxoCursor, bindArraySize), 0 },
    { "rowcount", T_ULONGLONG, offsetof(cxoCursor, rowCount), READONLY },
    { "statement", T_OBJECT, offsetof(cxoCursor, statement), READONLY },
    { "connection", T_OBJECT_EX, offsetof(cxoCursor, connection), READONLY },
    { "rowfactory", T_OBJECT, offsetof(cxoCursor, rowFactory), 0 },
    { "bindvars", T_OBJECT, offsetof(cxoCursor, bindVariables), READONLY },
    { "fetchvars", T_OBJECT, offsetof(cxoCursor, fetchVariables), READONLY },
    { "inputtypehandler", T_OBJECT, offsetof(cxoCursor, inputTypeHandler),
            0 },
    { "outputtypehandler", T_OBJECT, offsetof(cxoCursor, outputTypeHandler),
            0 },
    { "scrollable", T_BOOL, offsetof(cxoCursor, isScrollable), 0 },
    { NULL }
};


//-----------------------------------------------------------------------------
// declaration of calculated members for Python type
//-----------------------------------------------------------------------------
static PyGetSetDef cxoCalcMembers[] = {
    { "description", (getter) cxoCursor_getDescription, 0, 0, 0 },
    { "lastrowid", (getter) cxoCursor_getLastRowid, 0, 0, 0 },
    { "prefetchrows", (getter) cxoCursor_getPrefetchRows,
            (setter) cxoCursor_setPrefetchRows, 0, 0 },
    { NULL }
};


//-----------------------------------------------------------------------------
// declaration of Python type
//-----------------------------------------------------------------------------
PyTypeObject cxoPyTypeCursor = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "cx_Oracle.Cursor",
    .tp_basicsize = sizeof(cxoCursor),
    .tp_dealloc = (destructor) cxoCursor_free,
    .tp_repr = (reprfunc) cxoCursor_repr,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
    .tp_iter = (getiterfunc) cxoCursor_getIter,
    .tp_iternext = (iternextfunc) cxoCursor_getNext,
    .tp_methods = cxoMethods,
    .tp_members = cxoMembers,
    .tp_getset = cxoCalcMembers,
    .tp_init = (initproc) cxoCursor_init,
    .tp_new = cxoCursor_new
};
