From c61973c28f18264a2dfb66b077e36ac548a11552 Mon Sep 17 00:00:00 2001 From: Anthony Tuininga Date: Wed, 28 Mar 2018 16:24:41 -0600 Subject: [PATCH] Added support for DML returning of multiple rows using cursor.executemany(). --- doc/src/module.rst | 4 ++ odpi | 2 +- src/cxoCursor.c | 50 +---------------- src/cxoFuture.c | 4 ++ src/cxoModule.c | 1 - src/cxoModule.h | 5 +- src/cxoVar.c | 127 +++++++++++++++++++++++++++---------------- test/DMLReturning.py | 101 +++++++++++++++++++++++++++++++++- 8 files changed, 194 insertions(+), 100 deletions(-) diff --git a/doc/src/module.rst b/doc/src/module.rst index 146bbb4..a22db77 100644 --- a/doc/src/module.rst +++ b/doc/src/module.rst @@ -16,6 +16,10 @@ Module Interface the connection when the block is completed. This will become the default behavior in cx_Oracle 7. + - dml_ret_array_val -- if this value is True, variables bound to a DML + returning statement (and have not had any values set on them) will return + an array. This will become the default behavior in cx_Oracle 7. + All other attributes will silently ignore being set and will always appear to have the value None. diff --git a/odpi b/odpi index 1136e47..e4a722c 160000 --- a/odpi +++ b/odpi @@ -1 +1 @@ -Subproject commit 1136e4751571e6f5ccea93e321b169a6899409ec +Subproject commit e4a722cf100c8e28a07abf97a06f4f28fdd6cd20 diff --git a/src/cxoCursor.c b/src/cxoCursor.c index ef46c07..a905bff 100644 --- a/src/cxoCursor.c +++ b/src/cxoCursor.c @@ -46,7 +46,6 @@ static PyObject* cxoCursor_getBatchErrors(cxoCursor*); static PyObject *cxoCursor_getArrayDMLRowCounts(cxoCursor*); static PyObject *cxoCursor_getImplicitResults(cxoCursor*); static int cxoCursor_performDefine(cxoCursor*, uint32_t); -static int cxoCursor_getVarData(cxoCursor*); //----------------------------------------------------------------------------- @@ -878,7 +877,7 @@ static PyObject *cxoCursor_createRow(cxoCursor *cursor, uint32_t pos) // acquire the value for each item for (i = 0; i < numItems; i++) { var = (cxoVar*) PyList_GET_ITEM(cursor->fetchVariables, i); - item = cxoVar_getSingleValue(var, pos); + item = cxoVar_getSingleValue(var, var->data, pos); if (!item) { Py_DECREF(tuple); return NULL; @@ -1401,10 +1400,6 @@ static PyObject *cxoCursor_execute(cxoCursor *cursor, PyObject *args, return (PyObject*) cursor; } - // for returning statements, get the variable data for each bound variable - if (cursor->stmtInfo.isReturning && cxoCursor_getVarData(cursor) < 0) - return NULL; - // for statements other than queries, simply return None Py_RETURN_NONE; } @@ -1988,49 +1983,6 @@ static PyObject *cxoCursor_getNext(cxoCursor *cursor) } -//----------------------------------------------------------------------------- -// cxoCursor_getVarData() -// Get the data for all variables bound to the cursor. This is needed for a -// returning statement which may have changed the number of elements in the -// variable and the location of the variable data. -//----------------------------------------------------------------------------- -static int cxoCursor_getVarData(cxoCursor *cursor) -{ - Py_ssize_t i, size, pos; - PyObject *key, *value; - cxoVar *var; - - // if there are no bind variables, nothing to do - if (!cursor->bindVariables) - return 0; - - // handle bind by position - if (PyList_Check(cursor->bindVariables)) { - size = PyList_GET_SIZE(cursor->bindVariables); - for (i = 0; i < size; i++) { - var = (cxoVar*) PyList_GET_ITEM(cursor->bindVariables, i); - if (dpiVar_getData(var->handle, &var->allocatedElements, - &var->data) < 0) - return cxoError_raiseAndReturnInt(); - } - - // handle bind by name - } else { - pos = 0; - while (PyDict_Next(cursor->bindVariables, &pos, &key, &value)) { - var = (cxoVar*) value; - if (dpiVar_getData(var->handle, &var->allocatedElements, - &var->data) < 0) - return cxoError_raiseAndReturnInt(); - } - } - - return 0; -} - - - - //----------------------------------------------------------------------------- // cxoCursor_getBatchErrors() // Returns a list of batch error objects. diff --git a/src/cxoFuture.c b/src/cxoFuture.c index c7ae557..1f23a0c 100644 --- a/src/cxoFuture.c +++ b/src/cxoFuture.c @@ -69,6 +69,8 @@ static PyObject *cxoFuture_getAttr(cxoFuture *obj, PyObject *nameObject) return NULL; if (strncmp(buffer.ptr, "ctx_mgr_close", buffer.size) == 0) result = PyBool_FromLong(obj->contextManagerClose); + else if (strncmp(buffer.ptr, "dml_ret_array_val", buffer.size) == 0) + result = PyBool_FromLong(obj->dmlReturningArray); else { Py_INCREF(Py_None); result = Py_None; @@ -92,6 +94,8 @@ static int cxoFuture_setAttr(cxoFuture *obj, PyObject *nameObject, return -1; if (strncmp(buffer.ptr, "ctx_mgr_close", buffer.size) == 0) result = cxoUtils_getBooleanValue(value, 0, &obj->contextManagerClose); + else if (strncmp(buffer.ptr, "dml_ret_array_val", buffer.size) == 0) + result = cxoUtils_getBooleanValue(value, 0, &obj->dmlReturningArray); cxoBuffer_clear(&buffer); return result; } diff --git a/src/cxoModule.c b/src/cxoModule.c index 457fbac..87e8f82 100644 --- a/src/cxoModule.c +++ b/src/cxoModule.c @@ -392,7 +392,6 @@ static PyObject *cxoModule_initialize(void) cxoFutureObj = (cxoFuture*) cxoPyTypeFuture.tp_alloc(&cxoPyTypeFuture, 0); if (!cxoFutureObj) return NULL; - cxoFutureObj->contextManagerClose = 0; if (PyModule_AddObject(module, "__future__", (PyObject*) cxoFutureObj) < 0) return NULL; diff --git a/src/cxoModule.h b/src/cxoModule.h index 9a5b635..124a214 100644 --- a/src/cxoModule.h +++ b/src/cxoModule.h @@ -261,6 +261,7 @@ struct cxoEnqOptions { struct cxoFuture { PyObject_HEAD int contextManagerClose; + int dmlReturningArray; }; struct cxoLob { @@ -377,6 +378,8 @@ struct cxoVar { uint32_t size; uint32_t bufferSize; int isArray; + int isValueSet; + int getReturnedData; cxoVarType *type; }; @@ -457,7 +460,7 @@ cxoVarType *cxoVarType_fromPythonValue(PyObject *value, int *isArray, int cxoVar_bind(cxoVar *var, cxoCursor *cursor, PyObject *name, uint32_t pos); int cxoVar_check(PyObject *object); -PyObject *cxoVar_getSingleValue(cxoVar *var, uint32_t arrayPos); +PyObject *cxoVar_getSingleValue(cxoVar *var, dpiData *data, uint32_t arrayPos); PyObject *cxoVar_getValue(cxoVar *var, uint32_t arrayPos); cxoVar *cxoVar_new(cxoCursor *cursor, Py_ssize_t numElements, cxoVarType *type, Py_ssize_t size, int isArray, cxoObjectType *objType); diff --git a/src/cxoVar.c b/src/cxoVar.c index f39deda..a53ae3f 100644 --- a/src/cxoVar.c +++ b/src/cxoVar.c @@ -379,28 +379,81 @@ int cxoVar_bind(cxoVar *var, cxoCursor *cursor, PyObject *name, uint32_t pos) if (status < 0) return cxoError_raiseAndReturnInt(); + // set flag if bound to a DML returning statement and no data set + if (cursor->stmtInfo.isReturning && !var->isValueSet) + var->getReturnedData = 1; + return 0; } +//----------------------------------------------------------------------------- +// cxoVar_getArrayValue() +// Return the value of the variable as an array. +//----------------------------------------------------------------------------- +static PyObject *cxoVar_getArrayValue(cxoVar *var, uint32_t numElements, + dpiData *data) +{ + PyObject *value, *singleValue; + uint32_t i; + + // use the first set of returned values if DML returning as array is not + // enabled + if (!(cxoFutureObj && cxoFutureObj->dmlReturningArray) && + var->getReturnedData && !data) { + if (dpiVar_getReturnedData(var->handle, 0, &numElements, &data) < 0) + return cxoError_raiseAndReturnNull(); + } + + value = PyList_New(numElements); + if (!value) + return NULL; + + for (i = 0; i < numElements; i++) { + singleValue = cxoVar_getSingleValue(var, data, i); + if (!singleValue) { + Py_DECREF(value); + return NULL; + } + PyList_SET_ITEM(value, i, singleValue); + } + + return value; +} + + //----------------------------------------------------------------------------- // cxoVar_getSingleValue() // Return the value of the variable at the given position. //----------------------------------------------------------------------------- -PyObject *cxoVar_getSingleValue(cxoVar *var, uint32_t arrayPos) +PyObject *cxoVar_getSingleValue(cxoVar *var, dpiData *data, uint32_t arrayPos) { PyObject *value, *result; - dpiData *data; + uint32_t numReturnedRows; + dpiData *returnedData; - // ensure we do not exceed the number of allocated elements - if (arrayPos >= var->allocatedElements) { - PyErr_SetString(PyExc_IndexError, - "cxoVar_getSingleValue: array size exceeded"); - return NULL; + // handle DML returning + if (!data && var->getReturnedData) { + if (cxoFutureObj && cxoFutureObj->dmlReturningArray) { + if (dpiVar_getReturnedData(var->handle, arrayPos, &numReturnedRows, + &returnedData) < 0) + return cxoError_raiseAndReturnNull(); + return cxoVar_getArrayValue(var, numReturnedRows, returnedData); + } + if (dpiVar_getReturnedData(var->handle, 0, &numReturnedRows, + &data) < 0) + return cxoError_raiseAndReturnNull(); + if (arrayPos >= numReturnedRows) { + PyErr_SetString(PyExc_IndexError, + "cxoVar_getSingleValue: array size exceeded"); + return NULL; + } } - // return the value - data = &var->data[arrayPos]; + // in all other cases, just get the value stored at specified position + if (data) + data = &data[arrayPos]; + else data = &var->data[arrayPos]; if (data->isNull) Py_RETURN_NONE; value = cxoTransform_toPython(var->type->transformNum, var->connection, @@ -431,33 +484,6 @@ PyObject *cxoVar_getSingleValue(cxoVar *var, uint32_t arrayPos) } -//----------------------------------------------------------------------------- -// cxoVar_getArrayValue() -// Return the value of the variable as an array. -//----------------------------------------------------------------------------- -static PyObject *cxoVar_getArrayValue(cxoVar *var, - uint32_t numElements) -{ - PyObject *value, *singleValue; - uint32_t i; - - value = PyList_New(numElements); - if (!value) - return NULL; - - for (i = 0; i < numElements; i++) { - singleValue = cxoVar_getSingleValue(var, i); - if (!singleValue) { - Py_DECREF(value); - return NULL; - } - PyList_SET_ITEM(value, i, singleValue); - } - - return value; -} - - //----------------------------------------------------------------------------- // cxoVar_getValue() // Return the value of the variable. @@ -469,10 +495,14 @@ PyObject *cxoVar_getValue(cxoVar *var, uint32_t arrayPos) if (var->isArray) { if (dpiVar_getNumElementsInArray(var->handle, &numElements) < 0) return cxoError_raiseAndReturnNull(); - return cxoVar_getArrayValue(var, numElements); + return cxoVar_getArrayValue(var, numElements, var->data); } - - return cxoVar_getSingleValue(var, arrayPos); + if (arrayPos >= var->allocatedElements) { + PyErr_SetString(PyExc_IndexError, + "cxoVar_getSingleValue: array size exceeded"); + return NULL; + } + return cxoVar_getSingleValue(var, NULL, arrayPos); } @@ -659,6 +689,7 @@ static int cxoVar_setArrayValue(cxoVar *var, PyObject *value) //----------------------------------------------------------------------------- int cxoVar_setValue(cxoVar *var, uint32_t arrayPos, PyObject *value) { + var->isValueSet = 1; if (var->isArray) { if (arrayPos > 0) { PyErr_SetString(cxoNotSupportedErrorException, @@ -737,9 +768,10 @@ static PyObject *cxoVar_externalGetValue(cxoVar *var, PyObject *args, static PyObject *cxoVar_externalGetActualElements(cxoVar *var, void *unused) { - uint32_t numElements; + uint32_t numElements = var->allocatedElements; - if (dpiVar_getNumElementsInArray(var->handle, &numElements) < 0) + if (var->isArray && + dpiVar_getNumElementsInArray(var->handle, &numElements) < 0) return cxoError_raiseAndReturnNull(); return PyInt_FromLong(numElements); } @@ -751,11 +783,12 @@ static PyObject *cxoVar_externalGetActualElements(cxoVar *var, //----------------------------------------------------------------------------- static PyObject *cxoVar_externalGetValues(cxoVar *var, void *unused) { - uint32_t numElements; + uint32_t numElements = var->allocatedElements; - if (dpiVar_getNumElementsInArray(var->handle, &numElements) < 0) + if (var->isArray && + dpiVar_getNumElementsInArray(var->handle, &numElements) < 0) return cxoError_raiseAndReturnNull(); - return cxoVar_getArrayValue(var, numElements); + return cxoVar_getArrayValue(var, numElements, NULL); } @@ -771,10 +804,10 @@ static PyObject *cxoVar_repr(cxoVar *var) if (var->isArray) { if (dpiVar_getNumElementsInArray(var->handle, &numElements) < 0) return cxoError_raiseAndReturnNull(); - value = cxoVar_getArrayValue(var, numElements); + value = cxoVar_getArrayValue(var, numElements, var->data); } else if (var->allocatedElements == 1) - value = cxoVar_getSingleValue(var, 0); - else value = cxoVar_getArrayValue(var, var->allocatedElements); + value = cxoVar_getSingleValue(var, NULL, 0); + else value = cxoVar_getArrayValue(var, var->allocatedElements, NULL); if (!value) return NULL; if (cxoUtils_getModuleAndName(Py_TYPE(var), &module, &name) < 0) { diff --git a/test/DMLReturning.py b/test/DMLReturning.py index c6920c1..5385082 100644 --- a/test/DMLReturning.py +++ b/test/DMLReturning.py @@ -5,7 +5,7 @@ import sys class TestDMLReturning(BaseTestCase): def testInsert(self): - "test insert statement with DML returning" + "test insert statement (single row) with DML returning" self.cursor.execute("truncate table TestTempTable") intVal = 5 strVal = "A test string" @@ -21,6 +21,34 @@ class TestDMLReturning(BaseTestCase): strVar = strVar) self.assertEqual(intVar.values, [intVal]) self.assertEqual(strVar.values, [strVal]) + cx_Oracle.__future__.dml_ret_array_val = True + try: + self.assertEqual(intVar.values, [[intVal]]) + self.assertEqual(strVar.values, [[strVal]]) + finally: + cx_Oracle.__future__.dml_ret_array_val = False + + def testInsertMany(self): + "test insert statement (multiple rows) with DML returning" + self.cursor.execute("truncate table TestTempTable") + intValues = [5, 8, 17, 24, 6] + strValues = ["Test 5", "Test 8", "Test 17", "Test 24", "Test 6"] + intVar = self.cursor.var(cx_Oracle.NUMBER, arraysize = len(intValues)) + strVar = self.cursor.var(str, arraysize = len(intValues)) + self.cursor.setinputsizes(None, None, intVar, strVar) + data = list(zip(intValues, strValues)) + self.cursor.executemany(""" + insert into TestTempTable + values (:intVal, :strVal) + returning IntCol, StringCol into :intVar, :strVar""", data) + self.assertEqual(intVar.values, [intValues[0]]) + self.assertEqual(strVar.values, [strValues[0]]) + cx_Oracle.__future__.dml_ret_array_val = True + try: + self.assertEqual(intVar.values, [[v] for v in intValues]) + self.assertEqual(strVar.values, [[v] for v in strValues]) + finally: + cx_Oracle.__future__.dml_ret_array_val = False def testInsertWithSmallSize(self): "test insert statement with DML returning into too small a variable" @@ -57,6 +85,12 @@ class TestDMLReturning(BaseTestCase): strVar = strVar) self.assertEqual(intVar.values, [intVal]) self.assertEqual(strVar.values, [strVal]) + cx_Oracle.__future__.dml_ret_array_val = True + try: + self.assertEqual(intVar.values, [[intVal]]) + self.assertEqual(strVar.values, [[strVal]]) + finally: + cx_Oracle.__future__.dml_ret_array_val = False def testUpdateNoRows(self): "test update no rows statement with DML returning" @@ -78,6 +112,12 @@ class TestDMLReturning(BaseTestCase): strVar = strVar) self.assertEqual(intVar.values, []) self.assertEqual(strVar.values, []) + cx_Oracle.__future__.dml_ret_array_val = True + try: + self.assertEqual(intVar.values, [[]]) + self.assertEqual(strVar.values, [[]]) + finally: + cx_Oracle.__future__.dml_ret_array_val = False def testUpdateMultipleRows(self): "test update multiple rows statement with DML returning" @@ -101,6 +141,59 @@ class TestDMLReturning(BaseTestCase): "The final value of string 9", "The final value of string 10" ]) + cx_Oracle.__future__.dml_ret_array_val = True + try: + self.assertEqual(intVar.values, [[23, 24, 25]]) + self.assertEqual(strVar.values, [[ + "The final value of string 8", + "The final value of string 9", + "The final value of string 10" + ]]) + finally: + cx_Oracle.__future__.dml_ret_array_val = False + + def testUpdateMultipleRowsExecuteMany(self): + "test update multiple rows with DML returning (executeMany)" + self.cursor.execute("truncate table TestTempTable") + for i in range(1, 11): + self.cursor.execute("insert into TestTempTable values (:1, :2)", + (i, "The initial value of string %d" % i)) + intVar = self.cursor.var(cx_Oracle.NUMBER, arraysize = 3) + strVar = self.cursor.var(str, arraysize = 3) + self.cursor.setinputsizes(None, intVar, strVar) + self.cursor.executemany(""" + update TestTempTable set + IntCol = IntCol + 25, + StringCol = 'Updated value of string ' || to_char(IntCol) + where IntCol < :inVal + returning IntCol, StringCol into :intVar, :strVar""", + [[3], [8], [11]]) + self.assertEqual(intVar.values, [26, 27]) + self.assertEqual(strVar.values, [ + "Updated value of string 1", + "Updated value of string 2" + ]) + cx_Oracle.__future__.dml_ret_array_val = True + try: + self.assertEqual(intVar.values, [ + [26, 27], + [28, 29, 30, 31, 32], + [33, 34, 35] + ]) + self.assertEqual(strVar.values, [ + [ "Updated value of string 1", + "Updated value of string 2" ], + [ "Updated value of string 3", + "Updated value of string 4", + "Updated value of string 5", + "Updated value of string 6", + "Updated value of string 7" ], + [ "Updated value of string 8", + "Updated value of string 9", + "Updated value of string 10" ] + ]) + finally: + cx_Oracle.__future__.dml_ret_array_val = False def testInsertAndReturnObject(self): "test inserting an object with DML returning" @@ -116,5 +209,11 @@ class TestDMLReturning(BaseTestCase): obj = obj, outObj = outVar) result = outVar.getvalue() self.assertEqual(result.STRINGVALUE, stringValue) + cx_Oracle.__future__.dml_ret_array_val = True + try: + result, = outVar.getvalue() + self.assertEqual(result.STRINGVALUE, stringValue) + finally: + cx_Oracle.__future__.dml_ret_array_val = False self.connection.rollback()