From 39805c66d7a49010f71418ea2c05671e91820324 Mon Sep 17 00:00:00 2001 From: Anthony Tuininga Date: Thu, 11 Feb 2016 11:47:46 -0700 Subject: [PATCH] Added support for binding objects with sub objects attached to them; ensured that instances are created when an empty object variable is created; enhanced test cases for binding objects. --- Cursor.c | 4 +- Environment.c | 10 --- Object.c | 217 +++++++++++++++++++++++++++++++-------------- ObjectVar.c | 55 +++++++++++- cx_Oracle.c | 1 + test/ObjectVar.py | 88 ++++++++++++++++-- test/SetupTest.sql | 97 +++++++++++++++++++- test/uObjectVar.py | 34 +++++-- 8 files changed, 408 insertions(+), 98 deletions(-) diff --git a/Cursor.c b/Cursor.c index 84b4b33..5fd881b 100644 --- a/Cursor.c +++ b/Cursor.c @@ -2333,9 +2333,7 @@ static PyObject *Cursor_Var( // define the object type if needed if (type == (PyObject*) &g_ObjectVarType) { objectVar = (udt_ObjectVar*) var; - objectVar->objectType = ObjectType_NewByName(self->connection, - typeNameObj); - if (!objectVar->objectType) { + if (ObjectVar_SetType(objectVar, typeNameObj) < 0) { Py_DECREF(var); return NULL; } diff --git a/Environment.c b/Environment.c index 9129177..f0cbad1 100644 --- a/Environment.c +++ b/Environment.c @@ -18,8 +18,6 @@ typedef struct { udt_Buffer numberToStringFormatBuffer; udt_Buffer numberFromStringFormatBuffer; udt_Buffer nlsNumericCharactersBuffer; - OCIString *tempStringValue; - OCIDateTime *tempTimestampValue; } udt_Environment; //----------------------------------------------------------------------------- @@ -97,14 +95,6 @@ static udt_Environment *Environment_New( return NULL; } - // create the temp datetime handle (used for converting timestamps in - // objects) - status = OCIDescriptorAlloc(handle, (dvoid**) &env->tempTimestampValue, - OCI_DTYPE_TIMESTAMP, 0, 0); - if (Environment_CheckForError(env, status, - "Environment_New(): create timestamp descriptor") < 0) - return NULL; - return env; } diff --git a/Object.c b/Object.c index 1e1a7c8..b6bbd83 100644 --- a/Object.c +++ b/Object.c @@ -24,6 +24,7 @@ static int Object_SetAttr(udt_Object*, PyObject*, PyObject*); static PyObject *Object_ConvertToPython(udt_Environment*, OCITypeCode, dvoid*, dvoid*, udt_ObjectType*); + //----------------------------------------------------------------------------- // Declaration of external object variable members. //----------------------------------------------------------------------------- @@ -32,6 +33,7 @@ static PyMemberDef g_ObjectMembers[] = { { NULL } }; + //----------------------------------------------------------------------------- // Python type declaration //----------------------------------------------------------------------------- @@ -68,6 +70,41 @@ static PyTypeObject g_ObjectType = { }; +//----------------------------------------------------------------------------- +// Declaration of attribute data union +//----------------------------------------------------------------------------- +typedef union { + OCINumber numberValue; + OCIDate dateValue; + OCIDateTime *timestampValue; + OCIString *stringValue; +} udt_AttributeData; + + +//----------------------------------------------------------------------------- +// AttributeData_Free() +// Free any memory that was allocated by the convert from Python calls. +//----------------------------------------------------------------------------- +static void AttributeData_Free( + udt_Environment *environment, // environment object + udt_AttributeData *data, // data structure to initialize + OCITypeCode typeCode) // type of Oracle data +{ + switch (typeCode) { + case OCI_TYPECODE_CHAR: + case OCI_TYPECODE_VARCHAR: + case OCI_TYPECODE_VARCHAR2: + if (data->stringValue) + OCIStringResize(environment->handle, environment->errorHandle, + 0, &data->stringValue); + break; + case OCI_TYPECODE_TIMESTAMP: + if (data->timestampValue) + OCIDescriptorFree(data->timestampValue, OCI_DTYPE_TIMESTAMP); + }; +} + + //----------------------------------------------------------------------------- // Object_New() // Create a new object. @@ -184,6 +221,103 @@ static PyObject *Object_ConvertCollection( } +//----------------------------------------------------------------------------- +// Object_ConvertFromPython() +// Convert a Python value to an Oracle value. +//----------------------------------------------------------------------------- +static int Object_ConvertFromPython( + udt_Environment *environment, // environment to use + PyObject *pythonValue, // Python value to convert + OCITypeCode typeCode, // type of Oracle data + udt_AttributeData *oracleValue, // Oracle value + dvoid **ociValue, // OCI value + OCIInd *ociValueIndicator, // OCI value indicator + dvoid **ociObjectIndicator, // OCI object indicator + udt_ObjectType *subType) // sub type (for sub objects) +{ + udt_Object *objectValue; + udt_Buffer buffer; + sword status; + + // None is treated as null + if (pythonValue == Py_None) { + *ociValueIndicator = OCI_IND_NULL; + + // all other values need to be converted + } else { + + *ociValueIndicator = OCI_IND_NOTNULL; + switch (typeCode) { + case OCI_TYPECODE_CHAR: + case OCI_TYPECODE_VARCHAR: + case OCI_TYPECODE_VARCHAR2: + oracleValue->stringValue = NULL; + if (cxBuffer_FromObject(&buffer, pythonValue, + environment->encoding) < 0) + return -1; + status = OCIStringAssignText(environment->handle, + environment->errorHandle, buffer.ptr, + buffer.size, &oracleValue->stringValue); + cxBuffer_Clear(&buffer); + if (Environment_CheckForError(environment, status, + "Object_ConvertFromPython(): assigning string") < 0) + return -1; + *ociValue = oracleValue->stringValue; + break; + case OCI_TYPECODE_NUMBER: + if (PythonNumberToOracleNumber(environment, + pythonValue, &oracleValue->numberValue) < 0) + return -1; + *ociValue = &oracleValue->numberValue; + break; + case OCI_TYPECODE_DATE: + if (PythonDateToOracleDate(pythonValue, + &oracleValue->dateValue) < 0) + return -1; + *ociValue = &oracleValue->dateValue; + break; + case OCI_TYPECODE_TIMESTAMP: + oracleValue->timestampValue = NULL; + status = OCIDescriptorAlloc(environment->handle, + (dvoid**) &oracleValue->timestampValue, + OCI_DTYPE_TIMESTAMP, 0, 0); + if (Environment_CheckForError(environment, status, + "Object_ConvertFromPython(): " + "create timestamp descriptor") < 0) + return -1; + if (PythonDateToOracleTimestamp(environment, + pythonValue, oracleValue->timestampValue) < 0) + return -1; + *ociValue = oracleValue->timestampValue; + break; + case OCI_TYPECODE_OBJECT: + if (Py_TYPE(pythonValue) != &g_ObjectType) { + PyErr_SetString(PyExc_TypeError, + "expecting cx_Oracle.Object"); + return -1; + } + objectValue = (udt_Object*) pythonValue; + if (objectValue->objectType->tdo != subType->tdo) { + PyErr_SetString(PyExc_TypeError, + "expecting an object of the correct type"); + return -1; + } + *ociValue = objectValue->instance; + *ociObjectIndicator = objectValue->indicator; + break; + default: + PyErr_Format(g_NotSupportedErrorException, + "Object_ConvertFromPython(): unhandled data type %d", + typeCode); + return -1; + }; + + } + + return 0; +} + + //----------------------------------------------------------------------------- // Object_ConvertToPython() // Convert an Oracle value to a Python value. @@ -282,85 +416,38 @@ static int Object_SetAttributeValue( udt_ObjectAttribute *attribute, // attribute to set PyObject *value) // value to set { - dvoid *ociValueIndicator, *ociValue; - OCIInd ociScalarValueIndicator; + dvoid *ociObjectIndicator, *ociValue; + udt_AttributeData attributeData; + OCIInd ociValueIndicator; udt_Connection *connection; - OCINumber numericValue; udt_Buffer buffer; - OCIDate dateValue; sword status; - // initialization - ociValue = NULL; - ociValueIndicator = NULL; + // convert from Python + ociValue = ociObjectIndicator = NULL; connection = self->objectType->connection; - - // None is treated as null - if (value == Py_None) { - ociScalarValueIndicator = OCI_IND_NULL; - - // all other values need to be converted - } else { - - ociScalarValueIndicator = OCI_IND_NOTNULL; - switch (attribute->typeCode) { - case OCI_TYPECODE_CHAR: - case OCI_TYPECODE_VARCHAR: - case OCI_TYPECODE_VARCHAR2: - if (cxBuffer_FromObject(&buffer, value, - connection->environment->encoding) < 0) - return -1; - status = OCIStringAssignText(connection->environment->handle, - connection->environment->errorHandle, buffer.ptr, - buffer.size, - &connection->environment->tempStringValue); - cxBuffer_Clear(&buffer); - if (Environment_CheckForError(connection->environment, status, - "Object_SetAttributeValue(): assigning string") < 0) - return -1; - ociValue = connection->environment->tempStringValue; - break; - case OCI_TYPECODE_NUMBER: - ociValue = &numericValue; - if (PythonNumberToOracleNumber(connection->environment, value, - ociValue) < 0) - return -1; - break; - case OCI_TYPECODE_DATE: - ociValue = &dateValue; - if (PythonDateToOracleDate(value, ociValue) < 0) - return -1; - break; - case OCI_TYPECODE_TIMESTAMP: - ociValue = connection->environment->tempTimestampValue; - if (PythonDateToOracleTimestamp(connection->environment, value, - ociValue) < 0) - return -1; - break; - case OCI_TYPECODE_OBJECT: - break; - case OCI_TYPECODE_NAMEDCOLLECTION: - break; - }; - - if (!ociValue) { - PyErr_Format(g_NotSupportedErrorException, - "Object_SetAttributeValue(): unhandled data type %d", - attribute->typeCode); - return -1; - } - + if (Object_ConvertFromPython(connection->environment, value, + attribute->typeCode, &attributeData, &ociValue, &ociValueIndicator, + &ociObjectIndicator, attribute->subType) < 0) { + AttributeData_Free(connection->environment, &attributeData, + attribute->typeCode); + return -1; } // set the value for the attribute if (cxBuffer_FromObject(&buffer, attribute->name, - connection->environment->encoding) < 0) + connection->environment->encoding) < 0) { + AttributeData_Free(connection->environment, &attributeData, + attribute->typeCode); return -1; + } status = OCIObjectSetAttr(connection->environment->handle, connection->environment->errorHandle, self->instance, self->indicator, self->objectType->tdo, (const OraText**) &buffer.ptr, (ub4*) &buffer.size, 1, 0, 0, - ociScalarValueIndicator, ociValueIndicator, ociValue); + ociValueIndicator, ociObjectIndicator, ociValue); + AttributeData_Free(connection->environment, &attributeData, + attribute->typeCode); cxBuffer_Clear(&buffer); if (Environment_CheckForError(connection->environment, status, "Object_SetAttributeValue(): setting value") < 0) diff --git a/ObjectVar.c b/ObjectVar.c index 4e6159a..4a3a6f6 100644 --- a/ObjectVar.c +++ b/ObjectVar.c @@ -29,6 +29,7 @@ static int ObjectVar_PostDefine(udt_ObjectVar*); static int ObjectVar_PostBind(udt_ObjectVar*); static int ObjectVar_PreFetch(udt_ObjectVar*); static int ObjectVar_IsNull(udt_ObjectVar*, unsigned); +static int ObjectVar_SetType(udt_ObjectVar*, PyObject*); //----------------------------------------------------------------------------- // declaration of members for Oracle objects @@ -202,6 +203,11 @@ static int ObjectVar_PostBind( { sword status; + if (!self->objectType) { + PyErr_SetString(g_InterfaceErrorException, + "object type not associated with bind variable"); + return -1; + } status = OCIBindObject(self->bindHandle, self->environment->errorHandle, self->objectType->tdo, self->data, 0, self->objectIndicator, 0); return Environment_CheckForError(self->environment, status, @@ -264,7 +270,7 @@ static int ObjectVar_SetValue( if (!self->objectType) { Py_INCREF(object->objectType); self->objectType = object->objectType; - } else if (object->objectType != self->objectType) { + } else if (object->objectType->tdo != self->objectType->tdo) { PyErr_SetString(PyExc_TypeError, "expecting same type as the variable itself"); return -1; @@ -311,3 +317,50 @@ static PyObject *ObjectVar_GetValue( return self->objects[pos]; } + +//----------------------------------------------------------------------------- +// ObjectVar_SetType() +// Internal method used to set the type when creating an object variable. +// This will also create the object instances. +//----------------------------------------------------------------------------- +static int ObjectVar_SetType( + udt_ObjectVar *self, // variable to initialize the type + PyObject *typeNameObj) // value to set +{ + dvoid *instance, *indicator; + sword status; + ub4 i; + + // get the object type from the name + self->objectType = ObjectType_NewByName(self->connection, typeNameObj); + if (!self->objectType) + return -1; + + // initialize the object instances + for (i = 0; i < self->allocatedElements; i++) { + + // create the object instance + status = OCIObjectNew(self->connection->environment->handle, + self->connection->environment->errorHandle, + self->connection->handle, OCI_TYPECODE_OBJECT, + self->objectType->tdo, NULL, OCI_DURATION_SESSION, TRUE, + &instance); + if (Environment_CheckForError(self->connection->environment, status, + "ObjectVar_SetType(): create object instance") < 0) + return -1; + self->data[i] = instance; + + // get the null indicator structure + status = OCIObjectGetInd(self->connection->environment->handle, + self->connection->environment->errorHandle, instance, + &indicator); + if (Environment_CheckForError(self->connection->environment, status, + "ObjectVar_SetType(): get indicator structure") < 0) + return -1; + *((OCIInd*) indicator) = OCI_IND_NULL; + self->objectIndicator[i] = indicator; + } + + return 0; +} + diff --git a/cx_Oracle.c b/cx_Oracle.c index 2ad14b6..4005488 100644 --- a/cx_Oracle.c +++ b/cx_Oracle.c @@ -441,6 +441,7 @@ static PyObject *Module_Initialize(void) ADD_TYPE_OBJECT("Date", PyDateTimeAPI->DateType) ADD_TYPE_OBJECT("SessionPool", &g_SessionPoolType) ADD_TYPE_OBJECT("_Error", &g_ErrorType) + ADD_TYPE_OBJECT("Object", &g_ObjectType) // the name "connect" is required by the DB API ADD_TYPE_OBJECT("connect", &g_ConnectionType) diff --git a/test/ObjectVar.py b/test/ObjectVar.py index 3a5c190..db5114d 100644 --- a/test/ObjectVar.py +++ b/test/ObjectVar.py @@ -1,22 +1,77 @@ """Module for testing object variables.""" -import sys +import cx_Oracle +import datetime class TestObjectVar(BaseTestCase): + def __GetObjectAsTuple(self, obj): + attributeValues = [] + for attribute in obj.type.attributes: + value = getattr(obj, attribute.name) + if isinstance(value, cx_Oracle.Object): + value = self.__GetObjectAsTuple(value) + elif isinstance(value, list): + subValue = [] + for v in value: + if isinstance(v, cx_Oracle.Object): + v = self.__GetObjectAsTuple(v) + subValue.append(v) + value = subValue + attributeValues.append(value) + return tuple(attributeValues) + def __TestData(self, expectedIntValue, expectedObjectValue, expectedArrayValue): intValue, objectValue, arrayValue = self.cursor.fetchone() if objectValue is not None: - attributeValues = [] - for attribute in objectValue.type.attributes: - value = getattr(objectValue, attribute.name) - attributeValues.append(value) - objectValue = tuple(attributeValues) + objectValue = self.__GetObjectAsTuple(objectValue) self.assertEqual(intValue, expectedIntValue) self.assertEqual(objectValue, expectedObjectValue) self.assertEqual(arrayValue, expectedArrayValue) + def testBindNullIn(self): + "test binding a null value (IN)" + var = self.cursor.var(cx_Oracle.OBJECT, typename = "UDT_OBJECT") + result = self.cursor.callfunc("pkg_TestBindObject.GetStringRep", str, + (var,)) + self.assertEqual(result, "null") + + def testBindObjectIn(self): + "test binding an object (IN)" + typeObj = self.connection.gettype("UDT_OBJECT") + obj = typeObj.newobject() + obj.NUMBERVALUE = 13 + obj.STRINGVALUE = "Test String" + result = self.cursor.callfunc("pkg_TestBindObject.GetStringRep", str, + (obj,)) + self.assertEqual(result, + "udt_Object(13, 'Test String', null, null, null, null, null)") + obj.NUMBERVALUE = None + obj.STRINGVALUE = "Test With Dates" + obj.DATEVALUE = datetime.datetime(2016, 2, 10) + obj.TIMESTAMPVALUE = datetime.datetime(2016, 2, 10, 14, 13, 50) + result = self.cursor.callfunc("pkg_TestBindObject.GetStringRep", str, + (obj,)) + self.assertEqual(result, + "udt_Object(null, 'Test With Dates', null, " \ + "to_date('2016-02-10', 'YYYY-MM-DD'), " \ + "to_timestamp('2016-02-10 14:13:50', " \ + "'YYYY-MM-DD HH24:MI:SS'), " \ + "null, null)") + obj.DATEVALUE = None + obj.TIMESTAMPVALUE = None + subTypeObj = self.connection.gettype("UDT_SUBOBJECT") + subObj = subTypeObj.newobject() + subObj.SUBNUMBERVALUE = 15 + subObj.SUBSTRINGVALUE = "Sub String" + obj.SUBOBJECTVALUE = subObj + result = self.cursor.callfunc("pkg_TestBindObject.GetStringRep", str, + (obj,)) + self.assertEqual(result, + "udt_Object(null, 'Test With Dates', null, null, null, " \ + "udt_SubObject(15, 'Sub String'), null)") + def testFetchData(self): "test fetching objects" self.cursor.execute(""" @@ -32,11 +87,28 @@ class TestObjectVar(BaseTestCase): ('ARRAYCOL', cx_Oracle.OBJECT, -1, 2000, 0, 0, 1) ]) self.__TestData(1, (1, 'First row', 'First ', cx_Oracle.Timestamp(2007, 3, 6, 0, 0, 0), - cx_Oracle.Timestamp(2008, 9, 12, 16, 40)), [5, 10, None, 20]) + cx_Oracle.Timestamp(2008, 9, 12, 16, 40), + (11, 'Sub object 1'), + [(5, 'first element'), (6, 'second element')]), + [5, 10, None, 20]) self.__TestData(2, None, [3, None, 9, 12, 15]) self.__TestData(3, (3, 'Third row', 'Third ', cx_Oracle.Timestamp(2007, 6, 21, 0, 0, 0), - cx_Oracle.Timestamp(2007, 12, 13, 7, 30, 45)), None) + cx_Oracle.Timestamp(2007, 12, 13, 7, 30, 45), + (13, 'Sub object 3'), + [(10, 'element #1'), (20, 'element #2'), + (30, 'element #3'), (40, 'element #4')]), None) + + def testGetObjectType(self): + "test getting object type" + typeObj = self.connection.gettype("UDT_OBJECT") + self.assertEqual(typeObj.schema, "CX_ORACLE") + self.assertEqual(typeObj.name, "UDT_OBJECT") + expectedAttributeNames = ["NUMBERVALUE", "STRINGVALUE", + "FIXEDCHARVALUE", "DATEVALUE", "TIMESTAMPVALUE", + "SUBOBJECTVALUE", "SUBOBJECTARRAY"] + actualAttributeNames = [a.name for a in typeObj.attributes] + self.assertEqual(actualAttributeNames, expectedAttributeNames) def testObjectType(self): "test object type data" diff --git a/test/SetupTest.sql b/test/SetupTest.sql index af1a9cd..c7d2a10 100644 --- a/test/SetupTest.sql +++ b/test/SetupTest.sql @@ -38,12 +38,23 @@ grant to cx_Oracle; -- create types +create type cx_Oracle.udt_SubObject as object ( + SubNumberValue number, + SubStringValue varchar2(60) +); +/ + +create type cx_Oracle.udt_ObjectArray as varray(10) of cx_Oracle.udt_SubObject; +/ + create type cx_Oracle.udt_Object as object ( NumberValue number, StringValue varchar2(60), FixedCharValue char(10), DateValue date, - TimestampValue timestamp + TimestampValue timestamp, + SubObjectValue cx_Oracle.udt_SubObject, + SubObjectArray cx_Oracle.udt_ObjectArray ); / @@ -233,7 +244,11 @@ end; insert into cx_Oracle.TestObjects values (1, cx_Oracle.udt_Object(1, 'First row', 'First', to_date(20070306, 'YYYYMMDD'), - to_timestamp('20080912 16:40:00', 'YYYYMMDD HH24:MI:SS')), + to_timestamp('20080912 16:40:00', 'YYYYMMDD HH24:MI:SS'), + cx_Oracle.udt_SubObject(11, 'Sub object 1'), + cx_Oracle.udt_ObjectArray( + cx_Oracle.udt_SubObject(5, 'first element'), + cx_Oracle.udt_SubObject(6, 'second element'))), cx_Oracle.udt_Array(5, 10, null, 20)); insert into cx_Oracle.TestObjects values (2, null, @@ -242,7 +257,13 @@ insert into cx_Oracle.TestObjects values (2, null, insert into cx_Oracle.TestObjects values (3, cx_Oracle.udt_Object(3, 'Third row', 'Third', to_date(20070621, 'YYYYMMDD'), - to_timestamp('20071213 07:30:45', 'YYYYMMDD HH24:MI:SS')), null); + to_timestamp('20071213 07:30:45', 'YYYYMMDD HH24:MI:SS'), + cx_Oracle.udt_SubObject(13, 'Sub object 3'), + cx_Oracle.udt_ObjectArray( + cx_Oracle.udt_SubObject(10, 'element #1'), + cx_Oracle.udt_SubObject(20, 'element #2'), + cx_Oracle.udt_SubObject(30, 'element #3'), + cx_Oracle.udt_SubObject(40, 'element #4'))), null); commit; @@ -597,5 +618,75 @@ create or replace package body cx_Oracle.pkg_TestBooleans as end; / +create or replace package cx_Oracle.pkg_TestBindObject as + + function GetStringRep ( + a_Object udt_Object + ) return varchar2; + +end; +/ + +create or replace package body cx_Oracle.pkg_TestBindObject as + + function GetStringRep ( + a_Object udt_SubObject + ) return varchar2 is + begin + if a_Object is null then + return 'null'; + end if; + return 'udt_SubObject(' || + nvl(to_char(a_Object.SubNumberValue), 'null') || ', ' || + case when a_Object.SubStringValue is null then 'null' + else '''' || a_Object.SubStringValue || '''' end || ')'; + end; + + function GetStringRep ( + a_Array udt_ObjectArray + ) return varchar2 is + t_StringRep varchar2(4000); + begin + if a_Array is null then + return 'null'; + end if; + t_StringRep := 'udt_ObjectArray('; + for i in 1..a_Array.count loop + if i > 1 then + t_StringRep := t_StringRep || ', '; + end if; + t_StringRep := t_StringRep || GetStringRep(a_Array(i)); + end loop; + return t_StringRep || ')'; + end; + + function GetStringRep ( + a_Object udt_Object + ) return varchar2 is + begin + if a_Object is null then + return 'null'; + end if; + return 'udt_Object(' || + nvl(to_char(a_Object.NumberValue), 'null') || ', ' || + case when a_Object.StringValue is null then 'null' + else '''' || a_Object.StringValue || '''' end || ', ' || + case when a_Object.FixedCharValue is null then 'null' + else '''' || a_Object.FixedCharValue || '''' end || ', ' || + case when a_Object.DateValue is null then 'null' + else 'to_date(''' || + to_char(a_Object.DateValue, 'YYYY-MM-DD') || + ''', ''YYYY-MM-DD'')' end || ', ' || + case when a_Object.TimestampValue is null then 'null' + else 'to_timestamp(''' || to_char(a_Object.TimestampValue, + 'YYYY-MM-DD HH24:MI:SS') || + ''', ''YYYY-MM-DD HH24:MI:SS'')' end || ', ' || + GetStringRep(a_Object.SubObjectValue) || ', ' || + GetStringRep(a_Object.SubObjectArray) || ')'; + end; + +end; +/ + exit diff --git a/test/uObjectVar.py b/test/uObjectVar.py index 86202bf..317a8a8 100644 --- a/test/uObjectVar.py +++ b/test/uObjectVar.py @@ -1,18 +1,30 @@ """Module for testing object variables.""" -import sys +import cx_Oracle class TestObjectVar(BaseTestCase): + def __GetObjectAsTuple(self, obj): + attributeValues = [] + for attribute in obj.type.attributes: + value = getattr(obj, attribute.name) + if isinstance(value, cx_Oracle.Object): + value = self.__GetObjectAsTuple(value) + elif isinstance(value, list): + subValue = [] + for v in value: + if isinstance(v, cx_Oracle.Object): + v = self.__GetObjectAsTuple(v) + subValue.append(v) + value = subValue + attributeValues.append(value) + return tuple(attributeValues) + def __TestData(self, expectedIntValue, expectedObjectValue, expectedArrayValue): intValue, objectValue, arrayValue = self.cursor.fetchone() if objectValue is not None: - attributeValues = [] - for attribute in objectValue.type.attributes: - value = getattr(objectValue, attribute.name) - attributeValues.append(value) - objectValue = tuple(attributeValues) + objectValue = self.__GetObjectAsTuple(objectValue) self.assertEqual(intValue, expectedIntValue) self.assertEqual(objectValue, expectedObjectValue) self.assertEqual(arrayValue, expectedArrayValue) @@ -32,11 +44,17 @@ class TestObjectVar(BaseTestCase): (u'ARRAYCOL', cx_Oracle.OBJECT, -1, 2000, 0, 0, 1) ]) self.__TestData(1, (1, u'First row', u'First ', cx_Oracle.Timestamp(2007, 3, 6, 0, 0, 0), - cx_Oracle.Timestamp(2008, 9, 12, 16, 40)), [5, 10, None, 20]) + cx_Oracle.Timestamp(2008, 9, 12, 16, 40), + (11, 'Sub object 1'), + [(5, 'first element'), (6, 'second element')]), + [5, 10, None, 20]) self.__TestData(2, None, [3, None, 9, 12, 15]) self.__TestData(3, (3, u'Third row', u'Third ', cx_Oracle.Timestamp(2007, 6, 21, 0, 0, 0), - cx_Oracle.Timestamp(2007, 12, 13, 7, 30, 45)), None) + cx_Oracle.Timestamp(2007, 12, 13, 7, 30, 45), + (13, 'Sub object 3'), + [(10, 'element #1'), (20, 'element #2'), + (30, 'element #3'), (40, 'element #4')]), None) def testObjectType(self): "test object type data"