Added support for DML returning of multiple rows using cursor.executemany().

This commit is contained in:
Anthony Tuininga 2018-03-28 16:24:41 -06:00
parent b753ca7bdf
commit c61973c28f
8 changed files with 194 additions and 100 deletions

View File

@ -16,6 +16,10 @@ Module Interface
the connection when the block is completed. This will become the default the connection when the block is completed. This will become the default
behavior in cx_Oracle 7. 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 All other attributes will silently ignore being set and will always appear
to have the value None. to have the value None.

2
odpi

@ -1 +1 @@
Subproject commit 1136e4751571e6f5ccea93e321b169a6899409ec Subproject commit e4a722cf100c8e28a07abf97a06f4f28fdd6cd20

View File

@ -46,7 +46,6 @@ static PyObject* cxoCursor_getBatchErrors(cxoCursor*);
static PyObject *cxoCursor_getArrayDMLRowCounts(cxoCursor*); static PyObject *cxoCursor_getArrayDMLRowCounts(cxoCursor*);
static PyObject *cxoCursor_getImplicitResults(cxoCursor*); static PyObject *cxoCursor_getImplicitResults(cxoCursor*);
static int cxoCursor_performDefine(cxoCursor*, uint32_t); 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 // acquire the value for each item
for (i = 0; i < numItems; i++) { for (i = 0; i < numItems; i++) {
var = (cxoVar*) PyList_GET_ITEM(cursor->fetchVariables, i); var = (cxoVar*) PyList_GET_ITEM(cursor->fetchVariables, i);
item = cxoVar_getSingleValue(var, pos); item = cxoVar_getSingleValue(var, var->data, pos);
if (!item) { if (!item) {
Py_DECREF(tuple); Py_DECREF(tuple);
return NULL; return NULL;
@ -1401,10 +1400,6 @@ static PyObject *cxoCursor_execute(cxoCursor *cursor, PyObject *args,
return (PyObject*) cursor; 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 // for statements other than queries, simply return None
Py_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() // cxoCursor_getBatchErrors()
// Returns a list of batch error objects. // Returns a list of batch error objects.

View File

@ -69,6 +69,8 @@ static PyObject *cxoFuture_getAttr(cxoFuture *obj, PyObject *nameObject)
return NULL; return NULL;
if (strncmp(buffer.ptr, "ctx_mgr_close", buffer.size) == 0) if (strncmp(buffer.ptr, "ctx_mgr_close", buffer.size) == 0)
result = PyBool_FromLong(obj->contextManagerClose); result = PyBool_FromLong(obj->contextManagerClose);
else if (strncmp(buffer.ptr, "dml_ret_array_val", buffer.size) == 0)
result = PyBool_FromLong(obj->dmlReturningArray);
else { else {
Py_INCREF(Py_None); Py_INCREF(Py_None);
result = Py_None; result = Py_None;
@ -92,6 +94,8 @@ static int cxoFuture_setAttr(cxoFuture *obj, PyObject *nameObject,
return -1; return -1;
if (strncmp(buffer.ptr, "ctx_mgr_close", buffer.size) == 0) if (strncmp(buffer.ptr, "ctx_mgr_close", buffer.size) == 0)
result = cxoUtils_getBooleanValue(value, 0, &obj->contextManagerClose); 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); cxoBuffer_clear(&buffer);
return result; return result;
} }

View File

@ -392,7 +392,6 @@ static PyObject *cxoModule_initialize(void)
cxoFutureObj = (cxoFuture*) cxoPyTypeFuture.tp_alloc(&cxoPyTypeFuture, 0); cxoFutureObj = (cxoFuture*) cxoPyTypeFuture.tp_alloc(&cxoPyTypeFuture, 0);
if (!cxoFutureObj) if (!cxoFutureObj)
return NULL; return NULL;
cxoFutureObj->contextManagerClose = 0;
if (PyModule_AddObject(module, "__future__", (PyObject*) cxoFutureObj) < 0) if (PyModule_AddObject(module, "__future__", (PyObject*) cxoFutureObj) < 0)
return NULL; return NULL;

View File

@ -261,6 +261,7 @@ struct cxoEnqOptions {
struct cxoFuture { struct cxoFuture {
PyObject_HEAD PyObject_HEAD
int contextManagerClose; int contextManagerClose;
int dmlReturningArray;
}; };
struct cxoLob { struct cxoLob {
@ -377,6 +378,8 @@ struct cxoVar {
uint32_t size; uint32_t size;
uint32_t bufferSize; uint32_t bufferSize;
int isArray; int isArray;
int isValueSet;
int getReturnedData;
cxoVarType *type; 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_bind(cxoVar *var, cxoCursor *cursor, PyObject *name, uint32_t pos);
int cxoVar_check(PyObject *object); 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); PyObject *cxoVar_getValue(cxoVar *var, uint32_t arrayPos);
cxoVar *cxoVar_new(cxoCursor *cursor, Py_ssize_t numElements, cxoVarType *type, cxoVar *cxoVar_new(cxoCursor *cursor, Py_ssize_t numElements, cxoVarType *type,
Py_ssize_t size, int isArray, cxoObjectType *objType); Py_ssize_t size, int isArray, cxoObjectType *objType);

View File

@ -379,28 +379,81 @@ int cxoVar_bind(cxoVar *var, cxoCursor *cursor, PyObject *name, uint32_t pos)
if (status < 0) if (status < 0)
return cxoError_raiseAndReturnInt(); 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; 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() // cxoVar_getSingleValue()
// Return the value of the variable at the given position. // 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; PyObject *value, *result;
dpiData *data; uint32_t numReturnedRows;
dpiData *returnedData;
// ensure we do not exceed the number of allocated elements // handle DML returning
if (arrayPos >= var->allocatedElements) { if (!data && var->getReturnedData) {
PyErr_SetString(PyExc_IndexError, if (cxoFutureObj && cxoFutureObj->dmlReturningArray) {
"cxoVar_getSingleValue: array size exceeded"); if (dpiVar_getReturnedData(var->handle, arrayPos, &numReturnedRows,
return NULL; &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 // in all other cases, just get the value stored at specified position
data = &var->data[arrayPos]; if (data)
data = &data[arrayPos];
else data = &var->data[arrayPos];
if (data->isNull) if (data->isNull)
Py_RETURN_NONE; Py_RETURN_NONE;
value = cxoTransform_toPython(var->type->transformNum, var->connection, 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() // cxoVar_getValue()
// Return the value of the variable. // Return the value of the variable.
@ -469,10 +495,14 @@ PyObject *cxoVar_getValue(cxoVar *var, uint32_t arrayPos)
if (var->isArray) { if (var->isArray) {
if (dpiVar_getNumElementsInArray(var->handle, &numElements) < 0) if (dpiVar_getNumElementsInArray(var->handle, &numElements) < 0)
return cxoError_raiseAndReturnNull(); return cxoError_raiseAndReturnNull();
return cxoVar_getArrayValue(var, numElements); return cxoVar_getArrayValue(var, numElements, var->data);
} }
if (arrayPos >= var->allocatedElements) {
return cxoVar_getSingleValue(var, arrayPos); 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) int cxoVar_setValue(cxoVar *var, uint32_t arrayPos, PyObject *value)
{ {
var->isValueSet = 1;
if (var->isArray) { if (var->isArray) {
if (arrayPos > 0) { if (arrayPos > 0) {
PyErr_SetString(cxoNotSupportedErrorException, PyErr_SetString(cxoNotSupportedErrorException,
@ -737,9 +768,10 @@ static PyObject *cxoVar_externalGetValue(cxoVar *var, PyObject *args,
static PyObject *cxoVar_externalGetActualElements(cxoVar *var, static PyObject *cxoVar_externalGetActualElements(cxoVar *var,
void *unused) 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 cxoError_raiseAndReturnNull();
return PyInt_FromLong(numElements); return PyInt_FromLong(numElements);
} }
@ -751,11 +783,12 @@ static PyObject *cxoVar_externalGetActualElements(cxoVar *var,
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
static PyObject *cxoVar_externalGetValues(cxoVar *var, void *unused) 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 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 (var->isArray) {
if (dpiVar_getNumElementsInArray(var->handle, &numElements) < 0) if (dpiVar_getNumElementsInArray(var->handle, &numElements) < 0)
return cxoError_raiseAndReturnNull(); return cxoError_raiseAndReturnNull();
value = cxoVar_getArrayValue(var, numElements); value = cxoVar_getArrayValue(var, numElements, var->data);
} else if (var->allocatedElements == 1) } else if (var->allocatedElements == 1)
value = cxoVar_getSingleValue(var, 0); value = cxoVar_getSingleValue(var, NULL, 0);
else value = cxoVar_getArrayValue(var, var->allocatedElements); else value = cxoVar_getArrayValue(var, var->allocatedElements, NULL);
if (!value) if (!value)
return NULL; return NULL;
if (cxoUtils_getModuleAndName(Py_TYPE(var), &module, &name) < 0) { if (cxoUtils_getModuleAndName(Py_TYPE(var), &module, &name) < 0) {

View File

@ -5,7 +5,7 @@ import sys
class TestDMLReturning(BaseTestCase): class TestDMLReturning(BaseTestCase):
def testInsert(self): def testInsert(self):
"test insert statement with DML returning" "test insert statement (single row) with DML returning"
self.cursor.execute("truncate table TestTempTable") self.cursor.execute("truncate table TestTempTable")
intVal = 5 intVal = 5
strVal = "A test string" strVal = "A test string"
@ -21,6 +21,34 @@ class TestDMLReturning(BaseTestCase):
strVar = strVar) strVar = strVar)
self.assertEqual(intVar.values, [intVal]) self.assertEqual(intVar.values, [intVal])
self.assertEqual(strVar.values, [strVal]) 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): def testInsertWithSmallSize(self):
"test insert statement with DML returning into too small a variable" "test insert statement with DML returning into too small a variable"
@ -57,6 +85,12 @@ class TestDMLReturning(BaseTestCase):
strVar = strVar) strVar = strVar)
self.assertEqual(intVar.values, [intVal]) self.assertEqual(intVar.values, [intVal])
self.assertEqual(strVar.values, [strVal]) 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): def testUpdateNoRows(self):
"test update no rows statement with DML returning" "test update no rows statement with DML returning"
@ -78,6 +112,12 @@ class TestDMLReturning(BaseTestCase):
strVar = strVar) strVar = strVar)
self.assertEqual(intVar.values, []) self.assertEqual(intVar.values, [])
self.assertEqual(strVar.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): def testUpdateMultipleRows(self):
"test update multiple rows statement with DML returning" "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 9",
"The final value of string 10" "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): def testInsertAndReturnObject(self):
"test inserting an object with DML returning" "test inserting an object with DML returning"
@ -116,5 +209,11 @@ class TestDMLReturning(BaseTestCase):
obj = obj, outObj = outVar) obj = obj, outObj = outVar)
result = outVar.getvalue() result = outVar.getvalue()
self.assertEqual(result.STRINGVALUE, stringValue) 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() self.connection.rollback()