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
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.

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_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.

View File

@ -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;
}

View File

@ -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;

View File

@ -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);

View File

@ -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) {
// 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) {

View File

@ -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()