Added support for manipluating PL/SQL collections (both ordered and unordered).

This commit is contained in:
Anthony Tuininga 2016-02-16 15:51:01 -07:00
parent e9d5a70e25
commit 6f52f6ce39
4 changed files with 526 additions and 0 deletions

293
Object.c
View File

@ -26,8 +26,17 @@ static PyObject *Object_ConvertToPython(udt_Environment*, OCITypeCode, dvoid*,
static PyObject *Object_Append(udt_Object*, PyObject*);
static PyObject *Object_AsList(udt_Object*, PyObject*);
static PyObject *Object_Copy(udt_Object*, PyObject*);
static PyObject *Object_Delete(udt_Object*, PyObject*);
static PyObject *Object_Exists(udt_Object*, PyObject*);
static PyObject *Object_Extend(udt_Object*, PyObject*);
static PyObject *Object_GetElement(udt_Object*, PyObject*);
static PyObject *Object_GetFirstIndex(udt_Object*, PyObject*);
static PyObject *Object_GetLastIndex(udt_Object*, PyObject*);
static PyObject *Object_GetNextIndex(udt_Object*, PyObject*);
static PyObject *Object_GetPrevIndex(udt_Object*, PyObject*);
static PyObject *Object_GetSize(udt_Object*, PyObject*);
static PyObject *Object_SetElement(udt_Object*, PyObject*);
static PyObject *Object_Trim(udt_Object*, PyObject*);
//-----------------------------------------------------------------------------
@ -37,8 +46,17 @@ static PyMethodDef g_ObjectMethods[] = {
{ "append", (PyCFunction) Object_Append, METH_VARARGS },
{ "aslist", (PyCFunction) Object_AsList, METH_NOARGS },
{ "copy", (PyCFunction) Object_Copy, METH_NOARGS },
{ "delete", (PyCFunction) Object_Delete, METH_VARARGS },
{ "exists", (PyCFunction) Object_Exists, METH_VARARGS },
{ "extend", (PyCFunction) Object_Extend, METH_VARARGS },
{ "first", (PyCFunction) Object_GetFirstIndex, METH_NOARGS },
{ "getelement", (PyCFunction) Object_GetElement, METH_VARARGS },
{ "last", (PyCFunction) Object_GetLastIndex, METH_NOARGS },
{ "next", (PyCFunction) Object_GetNextIndex, METH_VARARGS },
{ "prev", (PyCFunction) Object_GetPrevIndex, METH_VARARGS },
{ "setelement", (PyCFunction) Object_SetElement, METH_VARARGS },
{ "size", (PyCFunction) Object_GetSize, METH_NOARGS },
{ "trim", (PyCFunction) Object_Trim, METH_VARARGS },
{ NULL, NULL }
};
@ -628,6 +646,60 @@ static PyObject *Object_Copy(
}
//-----------------------------------------------------------------------------
// Object_Delete()
// Delete the element at the specified index in the collection.
//-----------------------------------------------------------------------------
static PyObject *Object_Delete(
udt_Object *self, // object
PyObject *args) // arguments
{
udt_Environment *environment;
sword status;
sb4 index;
if (Object_CheckIsCollection(self) < 0)
return NULL;
if (!PyArg_ParseTuple(args, "i", &index))
return NULL;
environment = self->objectType->connection->environment;
status = OCITableDelete(environment->handle, environment->errorHandle,
index, self->instance);
if (Environment_CheckForError(environment, status, "Object_Delete()") < 0)
return NULL;
Py_RETURN_NONE;
}
//-----------------------------------------------------------------------------
// Object_Exists()
// Return true or false indicating if an element exists in the collection at
// the specified index.
//-----------------------------------------------------------------------------
static PyObject *Object_Exists(
udt_Object *self, // object
PyObject *args) // arguments
{
udt_Environment *environment;
boolean exists;
sword status;
sb4 index;
if (Object_CheckIsCollection(self) < 0)
return NULL;
if (!PyArg_ParseTuple(args, "i", &index))
return NULL;
environment = self->objectType->connection->environment;
status = OCITableExists(environment->handle, environment->errorHandle,
self->instance, index, &exists);
if (Environment_CheckForError(environment, status, "Object_Exists()") < 0)
return NULL;
if (exists)
Py_RETURN_TRUE;
Py_RETURN_FALSE;
}
//-----------------------------------------------------------------------------
// Object_Extend()
// Extend the collection by appending each of the items in the sequence.
@ -662,6 +734,149 @@ static PyObject *Object_Extend(
}
//-----------------------------------------------------------------------------
// Object_GetElement()
// Return the element at the given position in the collection.
//-----------------------------------------------------------------------------
static PyObject *Object_GetElement(
udt_Object *self, // object
PyObject *args) // arguments
{
void *elementValue, *elementIndicator;
udt_Environment *environment;
boolean exists;
sb4 position;
sword status;
if (Object_CheckIsCollection(self) < 0)
return NULL;
if (!PyArg_ParseTuple(args, "i", &position))
return NULL;
environment = self->objectType->connection->environment;
status = OCICollGetElem(environment->handle, environment->errorHandle,
(OCIColl*) self->instance, (sb4) position, &exists,
&elementValue, &elementIndicator);
if (Environment_CheckForError(environment, status,
"Object_GetItem(): get element") < 0)
return NULL;
if (!exists) {
PyErr_SetString(PyExc_IndexError, "element does not exist");
return NULL;
}
return Object_ConvertToPython(environment,
self->objectType->elementTypeCode, elementValue, elementIndicator,
(udt_ObjectType*) self->objectType->elementType);
}
//-----------------------------------------------------------------------------
// Object_GetFirstIndex()
// Return the index of the first entry in the collection.
//-----------------------------------------------------------------------------
static PyObject *Object_GetFirstIndex(
udt_Object *self, // object
PyObject *args) // arguments (none)
{
udt_Environment *environment;
sword status;
sb4 index;
if (Object_CheckIsCollection(self) < 0)
return NULL;
environment = self->objectType->connection->environment;
status = OCITableFirst(environment->handle, environment->errorHandle,
self->instance, &index);
if (Environment_CheckForError(environment, status,
"Object_GetFirstIndex()") < 0)
return NULL;
return PyInt_FromLong(index);
}
//-----------------------------------------------------------------------------
// Object_GetLastIndex()
// Return the index of the last entry in the collection.
//-----------------------------------------------------------------------------
static PyObject *Object_GetLastIndex(
udt_Object *self, // object
PyObject *args) // arguments (none)
{
udt_Environment *environment;
sword status;
sb4 index;
if (Object_CheckIsCollection(self) < 0)
return NULL;
environment = self->objectType->connection->environment;
status = OCITableLast(environment->handle, environment->errorHandle,
self->instance, &index);
if (Environment_CheckForError(environment, status,
"Object_GetLastIndex()") < 0)
return NULL;
return PyInt_FromLong(index);
}
//-----------------------------------------------------------------------------
// Object_GetNextIndex()
// Return the index of the next entry in the collection following the index
// specified. If there is no next entry, None is returned.
//-----------------------------------------------------------------------------
static PyObject *Object_GetNextIndex(
udt_Object *self, // object
PyObject *args) // arguments
{
udt_Environment *environment;
sb4 index, nextIndex;
boolean exists;
sword status;
if (Object_CheckIsCollection(self) < 0)
return NULL;
if (!PyArg_ParseTuple(args, "i", &index))
return NULL;
environment = self->objectType->connection->environment;
status = OCITableNext(environment->handle, environment->errorHandle,
index, self->instance, &nextIndex, &exists);
if (Environment_CheckForError(environment, status,
"Object_GetNextIndex()") < 0)
return NULL;
if (exists)
return PyInt_FromLong(nextIndex);
Py_RETURN_NONE;
}
//-----------------------------------------------------------------------------
// Object_GetPrevIndex()
// Return the index of the previous entry in the collection preceding the
// index specified. If there is no previous entry, None is returned.
//-----------------------------------------------------------------------------
static PyObject *Object_GetPrevIndex(
udt_Object *self, // object
PyObject *args) // arguments
{
udt_Environment *environment;
sb4 index, prevIndex;
boolean exists;
sword status;
if (Object_CheckIsCollection(self) < 0)
return NULL;
if (!PyArg_ParseTuple(args, "i", &index))
return NULL;
environment = self->objectType->connection->environment;
status = OCITablePrev(environment->handle, environment->errorHandle,
index, self->instance, &prevIndex, &exists);
if (Environment_CheckForError(environment, status,
"Object_GetPrevIndex()") < 0)
return NULL;
if (exists)
return PyInt_FromLong(prevIndex);
Py_RETURN_NONE;
}
//-----------------------------------------------------------------------------
// Object_GetSize()
// Return the size of a collection. If the object is not a collection, an
@ -685,3 +900,81 @@ static PyObject *Object_GetSize(
return PyInt_FromLong(size);
}
//-----------------------------------------------------------------------------
// Object_SetElement()
// Set the element at the specified location to the given value.
//-----------------------------------------------------------------------------
static PyObject *Object_SetElement(
udt_Object *self, // object
PyObject *args) // arguments
{
void *elementValue, *elementIndicator;
udt_AttributeData attributeData;
udt_Environment *environment;
OCIInd tempIndicator;
PyObject *value;
sb4 position;
sword status;
// make sure we are dealing with a collection
if (Object_CheckIsCollection(self) < 0)
return NULL;
// parse arguments
if (!PyArg_ParseTuple(args, "iO", &position, &value))
return NULL;
// convert to OCI value
elementValue = elementIndicator = NULL;
environment = self->objectType->connection->environment;
if (Object_ConvertFromPython(environment, value,
self->objectType->elementTypeCode, &attributeData, &elementValue,
&tempIndicator, &elementIndicator,
(udt_ObjectType*) self->objectType->elementType) < 0) {
AttributeData_Free(environment, &attributeData,
self->objectType->elementTypeCode);
return NULL;
}
if (!elementIndicator)
elementIndicator = &tempIndicator;
status = OCICollAssignElem(environment->handle, environment->errorHandle,
position, elementValue, elementIndicator,
(OCIColl*) self->instance);
if (Environment_CheckForError(environment, status,
"Object_SetItem(): assign element") < 0) {
AttributeData_Free(environment, &attributeData,
self->objectType->elementTypeCode);
return NULL;
}
AttributeData_Free(environment, &attributeData,
self->objectType->elementTypeCode);
Py_RETURN_NONE;
}
//-----------------------------------------------------------------------------
// Object_Trim()
// Trim a number of elements from the end of the collection.
//-----------------------------------------------------------------------------
static PyObject *Object_Trim(
udt_Object *self, // object
PyObject *args) // arguments
{
udt_Environment *environment;
sb4 numToTrim;
sword status;
if (Object_CheckIsCollection(self) < 0)
return NULL;
if (!PyArg_ParseTuple(args, "i", &numToTrim))
return NULL;
environment = self->objectType->connection->environment;
status = OCICollTrim(environment->handle, environment->errorHandle,
numToTrim, self->instance);
if (Environment_CheckForError(environment, status, "Object_Trim()") < 0)
return NULL;
Py_RETURN_NONE;
}

View File

@ -17,6 +17,12 @@ Object Type Objects
object type. Each attribute has a name attribute on it.
.. attribute:: ObjectType.iscollection
This read-only attribute returns a boolean indicating if the object type
refers to a collection or not.
.. attribute:: ObjectType.name
This read-only attribute returns the name of the type.
@ -33,3 +39,98 @@ Object Type Objects
This read-only attribute returns the name of the schema that owns the type.
Object Objects
==============
.. note::
This object is an extension to the DB API. It is returned by the
:meth:`~ObjectType.newobject()` call and can be bound to variables of
type cx_Oracle.OBJECT. Attributes can be retrieved and set directly.
.. method:: Object.append(element)
Append an element to the collection object. If no elements exist in the
collection, this creates an element at index 0; otherwise, it creates an
element immediately following the highest index available in the collection.
.. method:: Object.aslist()
Return a list of each of the collection's elements in index order.
.. method:: Object.copy()
Create a copy of the object and return it.
.. method:: Object.delete(index)
Delete the element at the specified index of the collection. If the
element does not exist or is otherwise invalid, an error is raised. Note
that the indices of the remaining elements in the collection are not
changed. In other words, the delete operation creates holes in the
collection.
.. method:: Object.exists(index)
Return True or False indicating if an element exists in the collection at
the specified index.
.. method:: Object.extend(sequence)
Append all of the elements in the sequence to the collection. This is
the equivalent of performing append() for each element found in the
sequence.
.. method:: Object.first()
Return the index of the first element in the collection. If the collection
is empty, an error is raised.
.. method:: Object.getelement(index)
Return the element at the specified index of the collection. If no element
exists at that index, IndexError is raised.
.. method:: Object.last()
Return the index of the last element in the collection. If the collection
is empty, an error is raised.
.. method:: Object.next(index)
Return the index of the next element in the collection following the
specified index. If there are no elements in the collection following the
specified index, None is returned.
.. method:: Object.prev(index)
Return the index of the element in the collection preceding the specified
index. If there are no elements in the collection preceding the
specified index, None is returned.
.. method:: Object.setelement(index, value)
Set the value in the collection at the specified index to the given value.
.. method:: Object.size()
Return the number of elements in the collection.
.. method:: Object.trim(num)
Remove the specified number of elements from the end of the collection.

View File

@ -1,5 +1,6 @@
"""Module for testing features introduced in 12.1"""
import datetime
import sys
if sys.version_info > (3,):
@ -40,6 +41,123 @@ class TestFeatures12_1(BaseTestCase):
count, = self.cursor.fetchone()
self.assertEqual(count, len(rows))
def testBindPLSQLDateCollectionIn(self):
"test binding a PL/SQL date collection (in)"
typeObj = self.connection.gettype("PKG_TESTDATEARRAYS.UDT_DATELIST")
obj = typeObj.newobject()
obj.setelement(1, datetime.datetime(2016, 2, 5))
obj.append(datetime.datetime(2016, 2, 8, 12, 15, 30))
obj.append(datetime.datetime(2016, 2, 12, 5, 44, 30))
result = self.cursor.callfunc("pkg_TestDateArrays.TestInArrays", int,
(2, datetime.datetime(2016, 2, 1), obj))
self.assertEqual(result, 24.75)
def testBindPLSQLDateCollectionInOut(self):
"test binding a PL/SQL date collection (in/out)"
typeObj = self.connection.gettype("PKG_TESTDATEARRAYS.UDT_DATELIST")
obj = typeObj.newobject()
obj.setelement(1, datetime.datetime(2016, 1, 1))
obj.append(datetime.datetime(2016, 1, 7))
obj.append(datetime.datetime(2016, 1, 13))
obj.append(datetime.datetime(2016, 1, 19))
self.cursor.callproc("pkg_TestDateArrays.TestInOutArrays", (4, obj))
self.assertEqual(obj.aslist(),
[datetime.datetime(2016, 1, 8),
datetime.datetime(2016, 1, 14),
datetime.datetime(2016, 1, 20),
datetime.datetime(2016, 1, 26)])
def testBindPLSQLDateCollectionOut(self):
"test binding a PL/SQL date collection (out)"
typeObj = self.connection.gettype("PKG_TESTDATEARRAYS.UDT_DATELIST")
obj = typeObj.newobject()
self.cursor.callproc("pkg_TestDateArrays.TestOutArrays", (3, obj))
self.assertEqual(obj.aslist(),
[datetime.datetime(2002, 12, 13, 4, 48),
datetime.datetime(2002, 12, 14, 9, 36),
datetime.datetime(2002, 12, 15, 14, 24)])
def testBindPLSQLNumberCollectionIn(self):
"test binding a PL/SQL number collection (in)"
typeObj = self.connection.gettype("PKG_TESTNUMBERARRAYS.UDT_NUMBERLIST")
obj = typeObj.newobject()
obj.setelement(1, 10)
obj.extend([20, 30, 40, 50])
result = self.cursor.callfunc("pkg_TestNumberArrays.TestInArrays", int,
(5, obj))
self.assertEqual(result, 155)
def testBindPLSQLNumberCollectionInOut(self):
"test binding a PL/SQL number collection (in/out)"
typeObj = self.connection.gettype("PKG_TESTNUMBERARRAYS.UDT_NUMBERLIST")
obj = typeObj.newobject()
obj.setelement(1, 5)
obj.extend([8, 3, 2])
self.cursor.callproc("pkg_TestNumberArrays.TestInOutArrays", (4, obj))
self.assertEqual(obj.aslist(), [50, 80, 30, 20])
def testBindPLSQLNumberCollectionOut(self):
"test binding a PL/SQL number collection (out)"
typeObj = self.connection.gettype("PKG_TESTNUMBERARRAYS.UDT_NUMBERLIST")
obj = typeObj.newobject()
self.cursor.callproc("pkg_TestNumberArrays.TestOutArrays", (3, obj))
self.assertEqual(obj.aslist(), [100, 200, 300])
def testBindPLSQLStringCollectionIn(self):
"test binding a PL/SQL string collection (in)"
typeObj = self.connection.gettype("PKG_TESTSTRINGARRAYS.UDT_STRINGLIST")
obj = typeObj.newobject()
obj.setelement(1, "First element")
obj.setelement(2, "Second element")
obj.setelement(3, "Third element")
result = self.cursor.callfunc("pkg_TestStringArrays.TestInArrays", int,
(5, obj))
self.assertEqual(result, 45)
def testBindPLSQLStringCollectionInOut(self):
"test binding a PL/SQL string collection (in/out)"
typeObj = self.connection.gettype("PKG_TESTSTRINGARRAYS.UDT_STRINGLIST")
obj = typeObj.newobject()
obj.setelement(1, "The first element")
obj.append("The second element")
obj.append("The third and final element")
self.cursor.callproc("pkg_TestStringArrays.TestInOutArrays", (3, obj))
self.assertEqual(obj.aslist(),
['Converted element # 1 originally had length 17',
'Converted element # 2 originally had length 18',
'Converted element # 3 originally had length 27'])
def testBindPLSQLStringCollectionOut(self):
"test binding a PL/SQL string collection (out)"
typeObj = self.connection.gettype("PKG_TESTSTRINGARRAYS.UDT_STRINGLIST")
obj = typeObj.newobject()
self.cursor.callproc("pkg_TestStringArrays.TestOutArrays", (4, obj))
self.assertEqual(obj.aslist(),
['Test out element # 1',
'Test out element # 2',
'Test out element # 3',
'Test out element # 4'])
def testBindPLSQLStringCollectionOutWithHoles(self):
"test binding a PL/SQL string collection (out with holes)"
typeObj = self.connection.gettype("PKG_TESTSTRINGARRAYS.UDT_STRINGLIST")
obj = typeObj.newobject()
self.cursor.callproc("pkg_TestStringArrays.TestIndexBy", (obj,))
self.assertEqual(obj.first(), -1048576)
self.assertEqual(obj.last(), 8388608)
self.assertEqual(obj.next(-576), 284)
self.assertEqual(obj.prev(284), -576)
self.assertEqual(obj.size(), 4)
self.assertEqual(obj.exists(-576), True)
self.assertEqual(obj.exists(-577), False)
self.assertEqual(obj.getelement(284), 'Third element')
self.assertEqual(obj.aslist(),
["First element", "Second element", "Third element",
"Fourth element"])
obj.delete(-576)
obj.delete(284)
self.assertEqual(obj.aslist(), ["First element", "Fourth element"])
def testExceptionInIteration(self):
"test executing with arraydmlrowcounts with exception"
self.cursor.execute("truncate table TestArrayDML")

View File

@ -322,6 +322,10 @@ create or replace package cx_Oracle.pkg_TestStringArrays as
a_Array out nocopy udt_StringList
);
procedure TestIndexBy (
a_Array out nocopy udt_StringList
);
end;
/
@ -362,6 +366,16 @@ create or replace package body cx_Oracle.pkg_TestStringArrays as
end loop;
end;
procedure TestIndexBy (
a_Array out nocopy udt_StringList
) is
begin
a_Array(-1048576) := 'First element';
a_Array(-576) := 'Second element';
a_Array(284) := 'Third element';
a_Array(8388608) := 'Fourth element';
end;
end;
/