diff --git a/doc/src/connection.rst b/doc/src/connection.rst index c44ff7b..8b72735 100644 --- a/doc/src/connection.rst +++ b/doc/src/connection.rst @@ -280,6 +280,26 @@ Connection Object This attribute is an extension to the DB API definition. +.. method:: Connection.getSodaDatabase() + + Return a :ref:`SodaDatabase ` object for Simple Oracle Document + Access (SODA). All SODA operations are performed either on the returned + SodaDatabase object or from objects created by SodaDatabase. See + `here `__ for + additional information on SODA. + + SODA support in cx_Oracle is in Preview status and should not be used in + production. It will be supported with a future version of Oracle Client + libraries. + + .. versionadded:: 7.0 + + .. note:: + + This method is an extension to the DB API definition. + + .. method:: Connection.gettype(name) Return a :ref:`type object ` given its name. This can then be diff --git a/doc/src/index.rst b/doc/src/index.rst index a21a63c..a525d91 100644 --- a/doc/src/index.rst +++ b/doc/src/index.rst @@ -24,6 +24,11 @@ Contents: lob.rst objecttype.rst aq.rst + sodadb.rst + sodacoll.rst + sodaop.rst + sodadoc.rst + sodadoccur.rst whatsnew.rst releasenotes.rst license.rst diff --git a/doc/src/sodacoll.rst b/doc/src/sodacoll.rst new file mode 100644 index 0000000..b9faaa6 --- /dev/null +++ b/doc/src/sodacoll.rst @@ -0,0 +1,118 @@ +.. _sodacoll: + +********************** +SODA Collection Object +********************** + +.. note:: + + This object is an extension the DB API. It is used to represent SODA + collections and is created by methods + :meth:`SodaDatabase.createCollection()` and + :meth:`SodaDatabase.openCollection()`. + + SODA support in cx_Oracle is in Preview status and should not be used in + production. It will be supported with a future version of Oracle Client + libraries. + + +.. method:: SodaCollection.createIndex(spec) + + Creates an index on a SODA collection. The spec is expected to be a + dictionary or a JSON-encoded string. See this `overview + `__ + for information on indexes in SODA. + + Note that a commit should be performed before attempting to create an + index. + + .. versionadded:: 7.0 + + +.. method:: SodaCollection.drop() + + Drops the collection from the database, if it exists. Note that if the + collection was created with mapMode set to True the underlying table will + not be dropped. + + A boolean value is returned indicating if the collection was actually + dropped. + + .. versionadded:: 7.0 + + +.. method:: SodaCollection.dropIndex(name, force=False) + + Drops the index with the specified name, if it exists. + + The force parameter, if set to True, can be used to force the dropping of + an index that the underlying Oracle Database domain index doesn't normally + permit. This is only applicable to spatial and JSON search indexes. + See `here `__ + for more information. + + A boolean value is returned indicating if the index was actually dropped. + + .. versionadded:: 7.0 + + +.. method:: SodaCollection.find() + + This method is used to begin an operation that will act upon documents in + the collection. It creates and returns a + :ref:`SodaOperation object ` which is used to specify the criteria + and the operation that will be performed on the documents that match that + criteria. + + .. versionadded:: 7.0 + + +.. method:: SodaCollection.getDataGuide() + + Returns a :ref:`SODA document object ` containing property names, + data types and lengths inferred from the JSON documents in the collection. + It can be useful for exploring the schema of a collection. Note that this + method is only supported for JSON-only collections where a JSON search + index has been created with the 'dataguide' option enabled. If there are + no documents in the collection, None is returned. + + .. versionadded:: 7.0 + + +.. method:: SodaCollection.insertOne(doc) + + Inserts a given document into the collection. The input document can be a + dictionary or list or an existing :ref:`SODA document object `. + + .. versionadded:: 7.0 + + +.. method:: SodaCollection.insertOneAndGet(doc) + + Similarly to :meth:`~SodaCollection.insertOne()` this method inserts a + given document into the collection. The only difference is that it + returns a :ref:`SODA Document object `. Note that for performance + reasons the returned document does not contain the content. + + .. versionadded:: 7.0 + + +.. attribute:: SodaCollection.metadata + + This read-only attribute returns a dicationary containing the metadata that + was used to create the collection. See this `collection metadata reference + `__ + for more information. + + .. versionadded:: 7.0 + + +.. attribute:: SodaCollection.name + + This read-only attribute returns the name of the collection. + + .. versionadded:: 7.0 + diff --git a/doc/src/sodadb.rst b/doc/src/sodadb.rst new file mode 100644 index 0000000..debc840 --- /dev/null +++ b/doc/src/sodadb.rst @@ -0,0 +1,90 @@ +.. _sodadb: + +******************** +SODA Database Object +******************** + +.. note:: + + This object is an extension the DB API. It is returned by the method + :meth:`Connection.getSodaDatabase()`. + + SODA support in cx_Oracle is in Preview status and should not be used in + production. It will be supported with a future version of Oracle Client + libraries. + + +.. method:: SodaDatabase.createCollection(name, metadata=None, mapMode=False) + + Creates a SODA collection with the given name and returns a new + :ref:`SODA collection object `. If you try to create a + collection, and a collection with the same name and metadata already + exists, then that existing collection is opened without error. + + If metadata is specified, it is expected to be a string containing valid + JSON or a dictionary that will be transformed into a JSON string. This JSON + permits you to specify the configuration of the collection including + storage options; specifying the presence or absence of columns for creation + timestamp, last modified timestamp and version; whether the collection can + store only JSON documents; and methods of key and version generation. The + default metadata creates a collection that only supports JSON documents and + uses system generated keys. See this `collection metadata reference + `__ + for more information. + + If the mapMode parameter is set to True, the new collection is mapped to an + existing table instead of creating a table. If a collection is created in + this way, dropping the collection will not drop the existing table either. + + .. versionadded:: 7.0 + + +.. method:: SodaDatabase.createDocument(content, key=None, mediaType="application/json") + + Creates a :ref:`Soda document ` usable for SODA write operations. + You only need to use this method if your collection requires + client-assigned keys or has non-JSON content; otherwise, you can pass your + content directly to SODA write operations. SodaDocument attributes + 'createdOn', 'lastModified' and 'version' will be None. + + The content parameter can be a dictionary or list which will be transformed + into a JSON string and then UTF-8 encoded. It can also be a string which + will be UTF-8 encoded or it can be a bytes object which will be stored + unchanged. If a bytes object is provided and the content is expected to be + JSON, note that SODA only supports UTF-8, UTF-16LE and UTF-16BE encodings. + + The key parameter should only be supplied if the collection in which the + document is to be placed requires client-assigned keys. + + The mediaType parameter should only be supplied if the collection in which + the document is to be placed supports non-JSON documents and the content + for this document is non-JSON. Using a standard MIME type for this value is + recommended but any string will be accepted. + + .. versionadded:: 7.0 + + +.. method:: SodaDatabase.getCollectionNames(startName=None, limit=0) + + Returns a list of the names of collections in the database that match the + criteria, in alphabetical order. + + If the startName parameter is specified, the list of names returned will + start with this value and also contain any names that fall after this value + in alphabetical order. + + If the limit parameter is specified and is non-zero, the number of + collection names returned will be limited to this value. + + .. versionadded:: 7.0 + + +.. method:: SodaDatabase.openCollection(name) + + Opens an existing collection with the given name and returns a new + :ref:`SODA collection object `. If a collection with that name + does not exist, None is returned. + + .. versionadded:: 7.0 + diff --git a/doc/src/sodadoc.rst b/doc/src/sodadoc.rst new file mode 100644 index 0000000..7ce0ac8 --- /dev/null +++ b/doc/src/sodadoc.rst @@ -0,0 +1,97 @@ +.. _sodadoc: + +******************** +SODA Document Object +******************** + +.. note:: + + This object is an extension the DB API. It is returned by the methods + :meth:`SodaDatabase.createDocument()`, + :meth:`SodaOperation.getDocuments()` and + :meth:`SodaOperation.getOne()` as well as by iterating over + :ref:`SODA document cursors `. + + SODA support in cx_Oracle is in Preview status and should not be used in + production. It will be supported with a future version of Oracle Client + libraries. + + +.. attribute:: SodaDoc.createdOn + + This read-only attribute returns the creation time of the document in + `ISO 8601 `__ + format. Documents created by :meth:`SodaDatabase.createDocument()` or + fetched from collections where this attribute is not stored will return + None. + + .. versionadded:: 7.0 + + +.. method:: SodaDoc.getContent() + + Returns the content of the document as a dictionary or list. This method + assumes that the content is application/json and will raise an exception if + this is not the case. If there is no content, however, None will be + returned. + + .. versionadded:: 7.0 + + +.. method:: SodaDoc.getContentAsBytes() + + Returns the content of the document as a bytes object. If there is no + content, however, None will be returned. + + .. versionadded:: 7.0 + + +.. method:: SodaDoc.getContentAsString() + + Returns the content of the document as a string. This method assumes that + the content is application/json and will raise an exception if this is not + the case. If there is no content, however, None will be returned. + + .. versionadded:: 7.0 + + +.. attribute:: SodaDoc.key + + This read-only attribute returns the unique key assigned to this document. + Documents created by :meth:`SodaDatabase.createDocument()` may not have a + value assigned to them and return None. + + .. versionadded:: 7.0 + + +.. attribute:: SodaDoc.lastModified + + This read-only attribute returns the last modified time of the document in + `ISO 8601 `__ + format. Documents created by :meth:`SodaDatabase.createDocument()` or + fetched from collections where this attribute is not stored will return + None. + + .. versionadded:: 7.0 + + +.. attribute:: SodaDoc.mediaType + + This read-only attribute returns the media type assigned to the document. + By convention this is expected to be a MIME type but no checks are + performed on this value. If a value is not specified when calling + :meth:`SodaDatabase.createDocument()` or the document is fetched from a + collection where this component is not stored, the string + "application/json" is returned. + + .. versionadded:: 7.0 + + +.. attribute:: SodaDoc.version + + This read-only attribute returns the version assigned to this document. + Documents created by :meth:`SodaDatabase.createDocument()` or fetched + from collections where this attribute is not stored will return None. + + .. versionadded:: 7.0 + diff --git a/doc/src/sodadoccur.rst b/doc/src/sodadoccur.rst new file mode 100644 index 0000000..56590b6 --- /dev/null +++ b/doc/src/sodadoccur.rst @@ -0,0 +1,25 @@ +.. _sodadoccur: + +*************************** +SODA Document Cursor Object +*************************** + +.. note:: + + This object is an extension the DB API. It is returned by the method + :meth:`SodaOperation.getCursor()` and implements the iterator protocol. + Each iteration will return a :ref:`SODA document object `. + + SODA support in cx_Oracle is in Preview status and should not be used in + production. It will be supported with a future version of Oracle Client + libraries. + + +.. method:: SodaDocCursor.close() + + Close the cursor now, rather than whenever __del__ is called. The cursor + will be unusable from this point forward; an Error exception will be raised + if any operation is attempted with the cursor. + + .. versionadded:: 7.0 + diff --git a/doc/src/sodaop.rst b/doc/src/sodaop.rst new file mode 100644 index 0000000..050a3a1 --- /dev/null +++ b/doc/src/sodaop.rst @@ -0,0 +1,165 @@ +.. _sodaop: + +********************* +SODA Operation Object +********************* + +.. note:: + + This object is an extension the DB API. It represents an operation that + will be performed on all or some of the documents in a SODA collection. It + is created by the method :meth:`SodaCollection.find()`. + + SODA support in cx_Oracle is in Preview status and should not be used in + production. It will be supported with a future version of Oracle Client + libraries. + + +.. method:: SodaOperation.count() + + Returns a count of the number of documents in the collection that match + the criteria. If :meth:`~SodaOperation.skip()` or + :meth:`~SodaOperation.limit()` were called on this object, an exception is + raised. + + .. versionadded:: 7.0 + + +.. method:: SodaOperation.filter(value) + + Sets a filter specification for complex document queries and ordering of + JSON documents. Filter specifications must be provided as a dictionary or + JSON-encoded string and can include comparisons, regular expressions, + logical and spatial operators, among others. See the + `overview of SODA filter specifications + `__ + for more information. + + As a convenience, the SodaOperation object is returned so that further + criteria can be specified by chaining methods together. + + .. versionadded:: 7.0 + + +.. method:: SodaOperation.getCursor() + + Returns a :ref:`SODA Document Cursor object ` that can be used + to iterate over the documents that match the criteria. + + .. versionadded:: 7.0 + + +.. method:: SodaOperation.getDocuments() + + Returns a list of :ref:`SODA Document objects ` that match the + criteria. + + .. versionadded:: 7.0 + + +.. method:: SodaOperation.getOne() + + Returns a single :ref:`SODA Document object ` that matches the + criteria. Note that if multiple documents match the criteria only the first + one is returned. + + .. versionadded:: 7.0 + + +.. method:: SodaOperation.key(value) + + Specifies that the document with the specified key should be returned. + This causes any previous calls made to this method and + :meth:`~SodaOperation.keys()` to be ignored. + + As a convenience, the SodaOperation object is returned so that further + criteria can be specified by chaining methods together. + + .. versionadded:: 7.0 + + +.. method:: SodaOperation.keys(seq) + + Specifies that documents that match the keys found in the supplied sequence + should be returned. This causes any previous calls made to this method and + :meth:`~SodaOperation.key()` to be ignored. + + As a convenience, the SodaOperation object is returned so that further + criteria can be specified by chaining methods together. + + .. versionadded:: 7.0 + + +.. method:: SodaOperation.limit(value) + + Specifies that only the specified number of documents should be returned. + This method is only usable for read operations such as + :meth:`~SodaOperation.getCursor()` and + :meth:`~SodaOperation.getDocuments()`. For write operations, any value set + using this method is ignored. + + As a convenience, the SodaOperation object is returned so that further + criteria can be specified by chaining methods together. + + .. versionadded:: 7.0 + + +.. method:: SodaOperation.remove() + + Removes all of the documents in the collection that match the criteria. The + number of documents that have been removed is returned. + + .. versionadded:: 7.0 + + +.. method:: SodaOperation.replaceOne(doc) + + Replaces a single document in the collection with the specified document. + The input document can be a dictionary or list or an existing + :ref:`SODA document object `. A boolean indicating if a document + was replaced or not is returned. + + Currently the method :meth:`~SodaOperation.key()` must be called before + this method can be called. + + .. versionadded:: 7.0 + + +.. method:: SodaOperation.replaceOneAndGet(doc) + + Similarly to :meth:`~SodaOperation.replaceOne()`, this method replaces a + single document in the collection with the specified document. The only + difference is that it returns a :ref:`SODA document object `. + Note that for performance reasons the returned document does not contain + the content. + + .. versionadded:: 7.0 + + +.. method:: SodaOperation.skip(value) + + Specifies the number of documents that match the other criteria that will + be skipped. This method is only usable for read operations such as + :meth:`~SodaOperation.getCursor()` and + :meth:`~SodaOperation.getDocuments()`. For write operations, any value set + using this method is ignored. + + As a convenience, the SodaOperation object is returned so that further + criteria can be specified by chaining methods together. + + .. versionadded:: 7.0 + + +.. method:: SodaOperation.version(value) + + Specifies that documents with the specified version should be returned. + Typically this is used with :meth:`~SodaOperation.key()` to implement + optimistic locking, so that the write operation called later does not + affect a document that someone else has modified. + + As a convenience, the SodaOperation object is returned so that further + criteria can be specified by chaining methods together. + + .. versionadded:: 7.0 + diff --git a/samples/SodaBasic.py b/samples/SodaBasic.py new file mode 100644 index 0000000..3403489 --- /dev/null +++ b/samples/SodaBasic.py @@ -0,0 +1,88 @@ +#------------------------------------------------------------------------------ +# Copyright 2018, Oracle and/or its affiliates. All rights reserved. +#------------------------------------------------------------------------------ + +#------------------------------------------------------------------------------ +# soda1.py +# A basic Simple Oracle Document Access (SODA) example. +# +# This script requires cx_Oracle 7.0 and higher. +# The user must have been granted the SODA_APP privilege +#------------------------------------------------------------------------------ + +from __future__ import print_function + +import cx_Oracle +import SampleEnv + +connection = cx_Oracle.Connection(SampleEnv.MAIN_CONNECT_STRING) + +# The general recommendation for simple SODA usage is to enable autocommit +connection.autocommit = True + +# Create the parent object for SODA +soda = connection.getSodaDatabase() + +# Create a new SODA collection and index +# This will open an existing collection, if the name is already in use. +collection = soda.createCollection("mycollection") + +indexSpec = { 'name': 'CITY_IDX', + 'fields': [ { + 'path': 'address.city', + 'datatype': 'string', + 'order': 'asc' } ] } +collection.createIndex(indexSpec) + +# Insert a documents. +# A system generated key is created by default. +content = {'name': 'Matilda', 'address': {'city': 'Melbourne'}} +doc = collection.insertOneAndGet(content) +key = doc.key +print('The key of the new SODA document is: ', key) + +# Fetch the document back +doc = collection.find().key(key).getOne() # A SodaDocument +content = doc.getContent() # A JavaScript object +print('Retrieved SODA document dictionary is:') +print(content) +content = doc.getContentAsString() # A JSON string +print('Retrieved SODA document string is:') +print(content) + +# Replace document contents +content = {'name': 'Matilda', 'address': {'city': 'Sydney'}} +collection.find().key(key).replaceOne(content) + +# Insert some more documents without caring about their keys +content = {'name': 'Venkat', 'address': {'city': 'Bengaluru'}} +collection.insertOne(content) +content = {'name': 'May', 'address': {'city': 'London'}} +collection.insertOne(content) +content = {'name': 'Sally-Ann', 'address': {'city': 'San Francisco'}} +collection.insertOne(content) + +# Find all documents with names like 'Ma%' +print("Names matching 'Ma%'") +documents = collection.find().filter({'name': {'$like': 'Ma%'}}).getDocuments() +for d in documents: + content = d.getContent() + print(content["name"]) + +# Count all documents +c = collection.find().count() +print('Collection has', c, 'documents') + +# Remove documents with cities containing 'o' +print('Removing documents') +c = collection.find().filter({'address.city': {'$regex': '.*o.*'}}).remove() +print('Dropped', c, 'documents') + +# Count all documents +c = collection.find().count() +print('Collection has', c, 'documents') + +# Drop the collection +if collection.drop(): + print('Collection was dropped') + diff --git a/samples/sql/SetupSamples.sql b/samples/sql/SetupSamples.sql index 91d1884..89480ff 100644 --- a/samples/sql/SetupSamples.sql +++ b/samples/sql/SetupSamples.sql @@ -39,6 +39,19 @@ to &main_user; grant execute on dbms_aqadm to &main_user; grant execute on dbms_lock to &main_user; +begin + + for r in + ( select role + from dba_roles + where role in ('SODA_APP') + ) loop + execute immediate 'grant ' || r.role || ' to &main_user'; + end loop; + +end; +/ + create user &edition_user identified by &edition_password; grant diff --git a/src/cxoConnection.c b/src/cxoConnection.c index f467490..c4e2535 100644 --- a/src/cxoConnection.c +++ b/src/cxoConnection.c @@ -50,6 +50,7 @@ static PyObject *cxoConnection_startup(cxoConnection*, PyObject*, PyObject*); static PyObject *cxoConnection_subscribe(cxoConnection*, PyObject*, PyObject*); static PyObject *cxoConnection_unsubscribe(cxoConnection*, PyObject*, PyObject*); +static PyObject *cxoConnection_getSodaDatabase(cxoConnection*, PyObject*); static PyObject *cxoConnection_getLTXID(cxoConnection*, void*); static PyObject *cxoConnection_getHandle(cxoConnection*, void*); static PyObject *cxoConnection_getCurrentSchema(cxoConnection*, void*); @@ -108,6 +109,8 @@ static PyMethodDef cxoConnectionMethods[] = { { "enq", (PyCFunction) cxoConnection_enqueue, METH_VARARGS | METH_KEYWORDS }, { "createlob", (PyCFunction) cxoConnection_createLob, METH_O }, + { "getSodaDatabase", (PyCFunction) cxoConnection_getSodaDatabase, + METH_NOARGS }, { NULL } }; @@ -498,6 +501,22 @@ static int cxoConnectionParams_finalize(cxoConnectionParams *params) } +//----------------------------------------------------------------------------- +// cxoConnection_getSodaFlags() +// Get the flags to use for SODA. This checks the autocommit flag and enables +// atomic commit if set to a true value. It also checks to ensure that the +// connection is valid. +//----------------------------------------------------------------------------- +int cxoConnection_getSodaFlags(cxoConnection *conn, uint32_t *flags) +{ + if (cxoConnection_isConnected(conn) < 0) + return -1; + *flags = (conn->autocommit) ? DPI_SODA_FLAGS_ATOMIC_COMMIT : + DPI_SODA_FLAGS_DEFAULT; + return 0; +} + + //----------------------------------------------------------------------------- // cxoConnection_isConnected() // Determines if the connection object is connected to the database. If not, @@ -1615,6 +1634,19 @@ static PyObject *cxoConnection_unsubscribe(cxoConnection *conn, PyObject* args, } +//----------------------------------------------------------------------------- +// cxoConnection_commit() +// Commit the transaction on the connection. +//----------------------------------------------------------------------------- +static PyObject *cxoConnection_getSodaDatabase(cxoConnection *conn, + PyObject *args) +{ + if (cxoConnection_isConnected(conn) < 0) + return NULL; + return (PyObject*) cxoSodaDatabase_new(conn); +} + + //----------------------------------------------------------------------------- // cxoConnection_getCurrentSchema() // Return the current schema associated with the connection. diff --git a/src/cxoModule.c b/src/cxoModule.c index 0b511b7..6bf5fc2 100644 --- a/src/cxoModule.c +++ b/src/cxoModule.c @@ -46,6 +46,8 @@ PyObject *cxoIntegrityErrorException = NULL; PyObject *cxoInternalErrorException = NULL; PyObject *cxoProgrammingErrorException = NULL; PyObject *cxoNotSupportedErrorException = NULL; +PyObject *cxoJsonDumpFunction = NULL; +PyObject *cxoJsonLoadFunction = NULL; cxoFuture *cxoFutureObj = NULL; dpiContext *cxoDpiContext = NULL; dpiVersionInfo cxoClientVersionInfo; @@ -277,6 +279,11 @@ static PyObject *cxoModule_initialize(void) CXO_MAKE_TYPE_READY(&cxoPyTypeObjectVar); CXO_MAKE_TYPE_READY(&cxoPyTypeRowidVar); CXO_MAKE_TYPE_READY(&cxoPyTypeSessionPool); + CXO_MAKE_TYPE_READY(&cxoPyTypeSodaCollection); + CXO_MAKE_TYPE_READY(&cxoPyTypeSodaDatabase); + CXO_MAKE_TYPE_READY(&cxoPyTypeSodaDoc); + CXO_MAKE_TYPE_READY(&cxoPyTypeSodaDocCursor); + CXO_MAKE_TYPE_READY(&cxoPyTypeSodaOperation); CXO_MAKE_TYPE_READY(&cxoPyTypeStringVar); CXO_MAKE_TYPE_READY(&cxoPyTypeSubscr); CXO_MAKE_TYPE_READY(&cxoPyTypeTimestampVar); @@ -339,6 +346,11 @@ static PyObject *cxoModule_initialize(void) CXO_ADD_TYPE_OBJECT("EnqOptions", &cxoPyTypeEnqOptions) CXO_ADD_TYPE_OBJECT("DeqOptions", &cxoPyTypeDeqOptions) CXO_ADD_TYPE_OBJECT("MessageProperties", &cxoPyTypeMsgProps) + CXO_ADD_TYPE_OBJECT("SodaCollection", &cxoPyTypeSodaCollection) + CXO_ADD_TYPE_OBJECT("SodaDatabase", &cxoPyTypeSodaDatabase) + CXO_ADD_TYPE_OBJECT("SodaDoc", &cxoPyTypeSodaDoc) + CXO_ADD_TYPE_OBJECT("SodaDocCursor", &cxoPyTypeSodaDocCursor) + CXO_ADD_TYPE_OBJECT("SodaOperation", &cxoPyTypeSodaOperation) // the name "connect" is required by the DB API CXO_ADD_TYPE_OBJECT("connect", &cxoPyTypeConnection) diff --git a/src/cxoModule.h b/src/cxoModule.h index b8202fa..682b52e 100644 --- a/src/cxoModule.h +++ b/src/cxoModule.h @@ -80,6 +80,11 @@ typedef struct cxoObject cxoObject; typedef struct cxoObjectAttr cxoObjectAttr; typedef struct cxoObjectType cxoObjectType; typedef struct cxoSessionPool cxoSessionPool; +typedef struct cxoSodaCollection cxoSodaCollection; +typedef struct cxoSodaDatabase cxoSodaDatabase; +typedef struct cxoSodaDoc cxoSodaDoc; +typedef struct cxoSodaDocCursor cxoSodaDocCursor; +typedef struct cxoSodaOperation cxoSodaOperation; typedef struct cxoSubscr cxoSubscr; typedef struct cxoVar cxoVar; typedef struct cxoVarType cxoVarType; @@ -137,6 +142,11 @@ extern PyTypeObject cxoPyTypeObjectType; extern PyTypeObject cxoPyTypeObjectVar; extern PyTypeObject cxoPyTypeRowidVar; extern PyTypeObject cxoPyTypeSessionPool; +extern PyTypeObject cxoPyTypeSodaCollection; +extern PyTypeObject cxoPyTypeSodaDatabase; +extern PyTypeObject cxoPyTypeSodaDoc; +extern PyTypeObject cxoPyTypeSodaDocCursor; +extern PyTypeObject cxoPyTypeSodaOperation; extern PyTypeObject cxoPyTypeStringVar; extern PyTypeObject cxoPyTypeSubscr; extern PyTypeObject cxoPyTypeTimestampVar; @@ -145,6 +155,10 @@ extern PyTypeObject cxoPyTypeTimestampVar; extern PyTypeObject *cxoPyTypeDate; extern PyTypeObject *cxoPyTypeDateTime; +// JSON dump and load functions for use with SODA +extern PyObject *cxoJsonDumpFunction; +extern PyObject *cxoJsonLoadFunction; + // ODPI-C context and version information extern dpiContext *cxoDpiContext; extern dpiVersionInfo cxoClientVersionInfo; @@ -353,6 +367,43 @@ struct cxoSessionPool { PyTypeObject *connectionType; }; +struct cxoSodaCollection { + PyObject_HEAD + dpiSodaColl *handle; + cxoSodaDatabase *db; + PyObject *name; +}; + +struct cxoSodaDatabase { + PyObject_HEAD + dpiSodaDb *handle; + cxoConnection *connection; +}; + +struct cxoSodaDoc { + PyObject_HEAD + cxoSodaDatabase *db; + dpiSodaDoc *handle; +}; + +struct cxoSodaDocCursor { + PyObject_HEAD + cxoSodaDatabase *db; + dpiSodaDocCursor *handle; +}; + +struct cxoSodaOperation { + PyObject_HEAD + cxoSodaCollection *coll; + dpiSodaOperOptions options; + uint32_t numKeyBuffers; + cxoBuffer *keyBuffers; + cxoBuffer keyBuffer; + cxoBuffer versionBuffer; + cxoBuffer filterBuffer; +}; + + struct cxoSubscr { PyObject_HEAD dpiSubscr *handle; @@ -403,6 +454,7 @@ struct cxoVarType { int cxoBuffer_fromObject(cxoBuffer *buf, PyObject *obj, const char *encoding); int cxoBuffer_init(cxoBuffer *buf); +int cxoConnection_getSodaFlags(cxoConnection *conn, uint32_t *flags); int cxoConnection_isConnected(cxoConnection *conn); int cxoCursor_performBind(cxoCursor *cursor); @@ -436,6 +488,18 @@ cxoObjectType *cxoObjectType_new(cxoConnection *connection, cxoObjectType *cxoObjectType_newByName(cxoConnection *connection, PyObject *name); +cxoSodaCollection *cxoSodaCollection_new(cxoSodaDatabase *db, + dpiSodaColl *handle); + +cxoSodaDatabase *cxoSodaDatabase_new(cxoConnection *connection); + +cxoSodaDoc *cxoSodaDoc_new(cxoSodaDatabase *db, dpiSodaDoc *handle); + +cxoSodaDocCursor *cxoSodaDocCursor_new(cxoSodaDatabase *db, + dpiSodaDocCursor *handle); + +cxoSodaOperation *cxoSodaOperation_new(cxoSodaCollection *collection); + void cxoSubscr_callback(cxoSubscr *subscr, dpiSubscrMessage *message); PyObject *cxoTransform_dateFromTicks(PyObject *args); @@ -459,6 +523,9 @@ int cxoUtils_getBooleanValue(PyObject *obj, int defaultValue, int *value); int cxoUtils_getModuleAndName(PyTypeObject *type, PyObject **module, PyObject **name); int cxoUtils_initializeDPI(void); +int cxoUtils_processJsonArg(PyObject *arg, cxoBuffer *buffer); +int cxoUtils_processSodaDocArg(cxoSodaDatabase *db, PyObject *arg, + cxoSodaDoc **doc); cxoVarType *cxoVarType_fromDataTypeInfo(dpiDataTypeInfo *info); cxoVarType *cxoVarType_fromPythonType(PyTypeObject *type); diff --git a/src/cxoSodaCollection.c b/src/cxoSodaCollection.c new file mode 100644 index 0000000..68dd6bd --- /dev/null +++ b/src/cxoSodaCollection.c @@ -0,0 +1,406 @@ +//----------------------------------------------------------------------------- +// Copyright 2018, Oracle and/or its affiliates. All rights reserved. +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// cxoSodaCollection.c +// Defines the routines for handling the SODA collection. +//----------------------------------------------------------------------------- + +#include "cxoModule.h" + +//----------------------------------------------------------------------------- +// declaration of functions +//----------------------------------------------------------------------------- +static void cxoSodaCollection_free(cxoSodaCollection*); +static PyObject *cxoSodaCollection_repr(cxoSodaCollection*); +static PyObject *cxoSodaCollection_createIndex(cxoSodaCollection*, PyObject*); +static PyObject *cxoSodaCollection_drop(cxoSodaCollection*, PyObject*); +static PyObject *cxoSodaCollection_dropIndex(cxoSodaCollection*, PyObject*, + PyObject*); +static PyObject *cxoSodaCollection_find(cxoSodaCollection*, PyObject*); +static PyObject *cxoSodaCollection_getDataGuide(cxoSodaCollection*, PyObject*); +static PyObject *cxoSodaCollection_insertOne(cxoSodaCollection*, PyObject*); +static PyObject *cxoSodaCollection_insertOneAndGet(cxoSodaCollection*, + PyObject*); +static PyObject *cxoSodaCollection_getMetadata(cxoSodaCollection*, PyObject*); + + +//----------------------------------------------------------------------------- +// declaration of methods +//----------------------------------------------------------------------------- +static PyMethodDef cxoMethods[] = { + { "createIndex", (PyCFunction) cxoSodaCollection_createIndex, METH_O }, + { "drop", (PyCFunction) cxoSodaCollection_drop, METH_NOARGS }, + { "dropIndex", (PyCFunction) cxoSodaCollection_dropIndex, + METH_VARARGS | METH_KEYWORDS }, + { "find", (PyCFunction) cxoSodaCollection_find, METH_NOARGS }, + { "getDataGuide", (PyCFunction) cxoSodaCollection_getDataGuide, + METH_NOARGS }, + { "insertOne", (PyCFunction) cxoSodaCollection_insertOne, METH_O }, + { "insertOneAndGet", (PyCFunction) cxoSodaCollection_insertOneAndGet, + METH_O }, + { NULL } +}; + + +//----------------------------------------------------------------------------- +// declaration of members +//----------------------------------------------------------------------------- +static PyMemberDef cxoMembers[] = { + { "name", T_OBJECT, offsetof(cxoSodaCollection, name), READONLY }, + { NULL } +}; + + +//----------------------------------------------------------------------------- +// declaration of calculated members +//----------------------------------------------------------------------------- +static PyGetSetDef cxoCalcMembers[] = { + { "metadata", (getter) cxoSodaCollection_getMetadata, 0, 0, 0 }, + { NULL } +}; + + +//----------------------------------------------------------------------------- +// Python type declarations +//----------------------------------------------------------------------------- +PyTypeObject cxoPyTypeSodaCollection = { + PyVarObject_HEAD_INIT(NULL, 0) + "cx_Oracle.SodaCollection", // tp_name + sizeof(cxoSodaCollection), // tp_basicsize + 0, // tp_itemsize + (destructor) cxoSodaCollection_free,// tp_dealloc + 0, // tp_print + 0, // tp_getattr + 0, // tp_setattr + 0, // tp_compare + (reprfunc) cxoSodaCollection_repr, // tp_repr + 0, // tp_as_number + 0, // tp_as_sequence + 0, // tp_as_mapping + 0, // tp_hash + 0, // tp_call + 0, // tp_str + 0, // tp_getattro + 0, // tp_setattro + 0, // tp_as_buffer + Py_TPFLAGS_DEFAULT, // tp_flags + 0, // tp_doc + 0, // tp_traverse + 0, // tp_clear + 0, // tp_richcompare + 0, // tp_weaklistoffset + 0, // tp_iter + 0, // tp_iternext + cxoMethods, // tp_methods + cxoMembers, // tp_members + cxoCalcMembers, // tp_getset + 0, // tp_base + 0, // tp_dict + 0, // tp_descr_get + 0, // tp_descr_set + 0, // tp_dictoffset + 0, // tp_init + 0, // tp_alloc + 0, // tp_new + 0, // tp_free + 0, // tp_is_gc + 0 // tp_bases +}; + +//----------------------------------------------------------------------------- +// cxoSodaCollection_initialize() +// Initialize a new collection with its attributes. +//----------------------------------------------------------------------------- +static int cxoSodaCollection_initialize(cxoSodaCollection *coll, + cxoSodaDatabase *db, const char *encoding, dpiSodaColl *handle) +{ + uint32_t nameLength; + const char *name; + + // get name from ODPI-C + if (dpiSodaColl_getName(handle, &name, &nameLength) < 0) + return cxoError_raiseAndReturnInt(); + coll->name = cxoPyString_fromEncodedString(name, nameLength, encoding, + NULL); + if (!coll->name) + return -1; + + // set base attributes (handle should not be added until there is no + // possibility of further failure) + coll->handle = handle; + Py_INCREF(db); + coll->db = db; + + return 0; +} + + +//----------------------------------------------------------------------------- +// cxoSodaCollection_new() +// Create a new SODA collection object. +//----------------------------------------------------------------------------- +cxoSodaCollection *cxoSodaCollection_new(cxoSodaDatabase *db, + dpiSodaColl *handle) +{ + cxoSodaCollection *coll; + + coll = (cxoSodaCollection*) + cxoPyTypeSodaCollection.tp_alloc(&cxoPyTypeSodaCollection, 0); + if (!coll) + return NULL; + if (cxoSodaCollection_initialize(coll, db, + db->connection->encodingInfo.encoding, handle) < 0) { + Py_DECREF(coll); + return NULL; + } + + return coll; +} + + +//----------------------------------------------------------------------------- +// cxoSodaCollection_free() +// Free the memory associated with a SODA collection. +//----------------------------------------------------------------------------- +static void cxoSodaCollection_free(cxoSodaCollection *coll) +{ + if (coll->handle) { + dpiSodaColl_release(coll->handle); + coll->handle = NULL; + } + Py_CLEAR(coll->db); + Py_CLEAR(coll->name); + Py_TYPE(coll)->tp_free((PyObject*) coll); +} + + +//----------------------------------------------------------------------------- +// cxoSodaCollection_repr() +// Return a string representation of a SODA collection. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaCollection_repr(cxoSodaCollection *coll) +{ + PyObject *module, *name, *result; + + if (cxoUtils_getModuleAndName(Py_TYPE(coll), &module, &name) < 0) + return NULL; + result = cxoUtils_formatString("<%s.%s %s>", + PyTuple_Pack(3, module, name, coll->name)); + Py_DECREF(module); + Py_DECREF(name); + return result; +} + + +//----------------------------------------------------------------------------- +// cxoSodaCollection_createIndex() +// Create an index on a SODA collection. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaCollection_createIndex(cxoSodaCollection *coll, + PyObject *specObj) +{ + cxoBuffer specBuffer; + uint32_t flags; + int status; + + if (cxoUtils_processJsonArg(specObj, &specBuffer) < 0) + return NULL; + if (cxoConnection_getSodaFlags(coll->db->connection, &flags) < 0) + return NULL; + Py_BEGIN_ALLOW_THREADS + status = dpiSodaColl_createIndex(coll->handle, specBuffer.ptr, + specBuffer.size, flags); + Py_END_ALLOW_THREADS + cxoBuffer_clear(&specBuffer); + if (status < 0) + return cxoError_raiseAndReturnNull(); + Py_RETURN_NONE; +} + + +//----------------------------------------------------------------------------- +// cxoSodaCollection_drop() +// Create a SODA collection and return it. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaCollection_drop(cxoSodaCollection *coll, + PyObject *args) +{ + uint32_t flags; + int isDropped; + + if (cxoConnection_getSodaFlags(coll->db->connection, &flags) < 0) + return NULL; + if (dpiSodaColl_drop(coll->handle, flags, &isDropped) < 0) + return cxoError_raiseAndReturnNull(); + if (isDropped) + Py_RETURN_TRUE; + Py_RETURN_FALSE; +} + + +//----------------------------------------------------------------------------- +// cxoSodaCollection_dropIndex() +// Drop an index on a SODA collection. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaCollection_dropIndex(cxoSodaCollection *coll, + PyObject *args, PyObject *keywordArgs) +{ + static char *keywordList[] = { "name", "force", NULL }; + int status, isDropped, force; + PyObject *nameObj, *forceObj; + cxoBuffer nameBuffer; + uint32_t flags; + + // parse arguments + forceObj = NULL; + if (!PyArg_ParseTupleAndKeywords(args, keywordArgs, "O|O", keywordList, + &nameObj, &forceObj)) + return NULL; + if (cxoUtils_getBooleanValue(forceObj, 0, &force) < 0) + return NULL; + + // drop index + if (cxoConnection_getSodaFlags(coll->db->connection, &flags) < 0) + return NULL; + if (force) + flags |= DPI_SODA_FLAGS_INDEX_DROP_FORCE; + if (cxoBuffer_fromObject(&nameBuffer, nameObj, + coll->db->connection->encodingInfo.encoding) < 0) + return NULL; + Py_BEGIN_ALLOW_THREADS + status = dpiSodaColl_dropIndex(coll->handle, nameBuffer.ptr, + nameBuffer.size, flags, &isDropped); + Py_END_ALLOW_THREADS + cxoBuffer_clear(&nameBuffer); + if (status < 0) + return cxoError_raiseAndReturnNull(); + if (isDropped) + Py_RETURN_TRUE; + Py_RETURN_FALSE; +} + + +//----------------------------------------------------------------------------- +// cxoSodaCollection_find() +// Creates an operation options object which can be used to perform a number +// of operations on the collection using the criteria set on the object. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaCollection_find(cxoSodaCollection *coll, + PyObject *args) +{ + return (PyObject*) cxoSodaOperation_new(coll); +} + + +//----------------------------------------------------------------------------- +// cxoSodaCollection_getDataGuide() +// Return the data guide associated with the collection. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaCollection_getDataGuide(cxoSodaCollection *coll, + PyObject *args) +{ + dpiSodaDoc *handle; + cxoSodaDoc *doc; + uint32_t flags; + int status; + + if (cxoConnection_getSodaFlags(coll->db->connection, &flags) < 0) + return NULL; + Py_BEGIN_ALLOW_THREADS + status = dpiSodaColl_getDataGuide(coll->handle, flags, &handle); + Py_END_ALLOW_THREADS + if (status < 0) + return cxoError_raiseAndReturnNull(); + if (handle) { + doc = cxoSodaDoc_new(coll->db, handle); + if (!doc) + return NULL; + return (PyObject*) doc; + } + Py_RETURN_NONE; +} + + +//----------------------------------------------------------------------------- +// cxoSodaCollection_insertOne() +// Insert a single document into the collection. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaCollection_insertOne(cxoSodaCollection *coll, + PyObject *arg) +{ + cxoSodaDoc *doc; + uint32_t flags; + int status; + + if (cxoUtils_processSodaDocArg(coll->db, arg, &doc) < 0) + return NULL; + if (cxoConnection_getSodaFlags(coll->db->connection, &flags) < 0) + return NULL; + Py_BEGIN_ALLOW_THREADS + status = dpiSodaColl_insertOne(coll->handle, doc->handle, flags, NULL); + Py_END_ALLOW_THREADS + if (status < 0) { + cxoError_raiseAndReturnNull(); + Py_DECREF(doc); + return NULL; + } + Py_DECREF(doc); + Py_RETURN_NONE; +} + + +//----------------------------------------------------------------------------- +// cxoSodaCollection_insertOneAndGet() +// Insert a single document into the collection and return a document +// containing all but the content itself. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaCollection_insertOneAndGet(cxoSodaCollection *coll, + PyObject *arg) +{ + dpiSodaDoc *returnedDoc; + cxoSodaDoc *doc; + uint32_t flags; + int status; + + if (cxoUtils_processSodaDocArg(coll->db, arg, &doc) < 0) + return NULL; + if (cxoConnection_getSodaFlags(coll->db->connection, &flags) < 0) + return NULL; + Py_BEGIN_ALLOW_THREADS + status = dpiSodaColl_insertOne(coll->handle, doc->handle, flags, + &returnedDoc); + Py_END_ALLOW_THREADS + if (status < 0) { + cxoError_raiseAndReturnNull(); + Py_DECREF(doc); + return NULL; + } + Py_DECREF(doc); + return (PyObject*) cxoSodaDoc_new(coll->db, returnedDoc); +} + + +//----------------------------------------------------------------------------- +// cxoSodaCollection_getMetadata() +// Retrieve the metadata for the collection. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaCollection_getMetadata(cxoSodaCollection *coll, + PyObject *unused) +{ + PyObject *str, *result; + uint32_t valueLength; + const char *value; + + if (dpiSodaColl_getMetadata(coll->handle, &value, &valueLength) < 0) + return cxoError_raiseAndReturnNull(); + str = PyUnicode_Decode(value, valueLength, + coll->db->connection->encodingInfo.encoding, NULL); + if (!str) + return NULL; + result = PyObject_CallFunctionObjArgs(cxoJsonLoadFunction, str, NULL); + Py_DECREF(str); + return result; +} + diff --git a/src/cxoSodaDatabase.c b/src/cxoSodaDatabase.c new file mode 100644 index 0000000..206fa1d --- /dev/null +++ b/src/cxoSodaDatabase.c @@ -0,0 +1,381 @@ +//----------------------------------------------------------------------------- +// Copyright 2018, Oracle and/or its affiliates. All rights reserved. +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// cxoSodaDatabase.c +// Defines the routines for handling the SODA database. +//----------------------------------------------------------------------------- + +#include "cxoModule.h" + +//----------------------------------------------------------------------------- +// Declaration of functions +//----------------------------------------------------------------------------- +static void cxoSodaDatabase_free(cxoSodaDatabase*); +static PyObject *cxoSodaDatabase_repr(cxoSodaDatabase*); +static PyObject *cxoSodaDatabase_createCollection(cxoSodaDatabase*, + PyObject*, PyObject*); +static PyObject *cxoSodaDatabase_createDocument(cxoSodaDatabase*, + PyObject*, PyObject*); +static PyObject *cxoSodaDatabase_getCollectionNames(cxoSodaDatabase*, + PyObject*, PyObject*); +static PyObject *cxoSodaDatabase_openCollection(cxoSodaDatabase*, PyObject*); + + +//----------------------------------------------------------------------------- +// declaration of methods for Python type "SodaDatabase" +//----------------------------------------------------------------------------- +static PyMethodDef cxoMethods[] = { + { "createCollection", (PyCFunction) cxoSodaDatabase_createCollection, + METH_VARARGS | METH_KEYWORDS }, + { "createDocument", (PyCFunction) cxoSodaDatabase_createDocument, + METH_VARARGS | METH_KEYWORDS }, + { "getCollectionNames", (PyCFunction) cxoSodaDatabase_getCollectionNames, + METH_VARARGS | METH_KEYWORDS }, + { "openCollection", (PyCFunction) cxoSodaDatabase_openCollection, METH_O }, + { NULL } +}; + + +//----------------------------------------------------------------------------- +// Python type declarations +//----------------------------------------------------------------------------- +PyTypeObject cxoPyTypeSodaDatabase = { + PyVarObject_HEAD_INIT(NULL, 0) + "cx_Oracle.SodaDatabase", // tp_name + sizeof(cxoSodaDatabase), // tp_basicsize + 0, // tp_itemsize + (destructor) cxoSodaDatabase_free, // tp_dealloc + 0, // tp_print + 0, // tp_getattr + 0, // tp_setattr + 0, // tp_compare + (reprfunc) cxoSodaDatabase_repr, // tp_repr + 0, // tp_as_number + 0, // tp_as_sequence + 0, // tp_as_mapping + 0, // tp_hash + 0, // tp_call + 0, // tp_str + 0, // tp_getattro + 0, // tp_setattro + 0, // tp_as_buffer + Py_TPFLAGS_DEFAULT, // tp_flags + 0, // tp_doc + 0, // tp_traverse + 0, // tp_clear + 0, // tp_richcompare + 0, // tp_weaklistoffset + 0, // tp_iter + 0, // tp_iternext + cxoMethods, // tp_methods + 0, // tp_members + 0, // tp_getset + 0, // tp_base + 0, // tp_dict + 0, // tp_descr_get + 0, // tp_descr_set + 0, // tp_dictoffset + 0, // tp_init + 0, // tp_alloc + 0, // tp_new + 0, // tp_free + 0, // tp_is_gc + 0 // tp_bases +}; + + +//----------------------------------------------------------------------------- +// cxoSodaDatabase_new() +// Create a new SODA database object. +//----------------------------------------------------------------------------- +cxoSodaDatabase *cxoSodaDatabase_new(cxoConnection *connection) +{ + cxoSodaDatabase *db; + PyObject *module; + + // load JSON dump/load functions, if needed + if (!cxoJsonDumpFunction || !cxoJsonLoadFunction) { + module = PyImport_ImportModule("json"); + if (!module) + return NULL; + if (!cxoJsonDumpFunction) { + cxoJsonDumpFunction = PyObject_GetAttrString(module, "dumps"); + if (!cxoJsonDumpFunction) + return NULL; + } + if (!cxoJsonLoadFunction) { + cxoJsonLoadFunction = PyObject_GetAttrString(module, "loads"); + if (!cxoJsonLoadFunction) + return NULL; + } + } + + // create SODA database object + db = (cxoSodaDatabase*) + cxoPyTypeSodaDatabase.tp_alloc(&cxoPyTypeSodaDatabase, 0); + if (!db) + return NULL; + if (dpiConn_getSodaDb(connection->handle, &db->handle) < 0) { + Py_DECREF(db); + cxoError_raiseAndReturnNull(); + return NULL; + } + Py_INCREF(connection); + db->connection = connection; + + return db; +} + + +//----------------------------------------------------------------------------- +// cxoSodaDatabase_free() +// Free the memory associated with a SODA database. +//----------------------------------------------------------------------------- +static void cxoSodaDatabase_free(cxoSodaDatabase *db) +{ + if (db->handle) { + dpiSodaDb_release(db->handle); + db->handle = NULL; + } + Py_CLEAR(db->connection); + Py_TYPE(db)->tp_free((PyObject*) db); +} + + +//----------------------------------------------------------------------------- +// cxoSodaDatabase_repr() +// Return a string representation of a SODA database. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaDatabase_repr(cxoSodaDatabase *db) +{ + PyObject *connectionRepr, *module, *name, *result; + + connectionRepr = PyObject_Repr((PyObject*) db->connection); + if (!connectionRepr) + return NULL; + if (cxoUtils_getModuleAndName(Py_TYPE(db), &module, &name) < 0) { + Py_DECREF(connectionRepr); + return NULL; + } + result = cxoUtils_formatString("<%s.%s on %s>", + PyTuple_Pack(3, module, name, connectionRepr)); + Py_DECREF(module); + Py_DECREF(name); + Py_DECREF(connectionRepr); + return result; +} + + +//----------------------------------------------------------------------------- +// cxoSodaDatabase_createCollection() +// Create a SODA collection and return it. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaDatabase_createCollection(cxoSodaDatabase *db, + PyObject *args, PyObject *keywordArgs) +{ + static char *keywordList[] = { "name", "metadata", "mapMode", NULL }; + PyObject *nameObj, *metadataObj, *mapModeObj; + cxoBuffer nameBuffer, metadataBuffer; + cxoSodaCollection *coll; + const char *encoding; + dpiSodaColl *handle; + int status, mapMode; + uint32_t flags; + + // parse arguments + nameObj = metadataObj = mapModeObj = NULL; + if (!PyArg_ParseTupleAndKeywords(args, keywordArgs, "O|OO", keywordList, + &nameObj, &metadataObj, &mapModeObj)) + return NULL; + encoding = db->connection->encodingInfo.encoding; + if (cxoBuffer_fromObject(&nameBuffer, nameObj, encoding) < 0) + return NULL; + if (cxoUtils_processJsonArg(metadataObj, &metadataBuffer) < 0) { + cxoBuffer_clear(&nameBuffer); + return NULL; + } + if (cxoUtils_getBooleanValue(mapModeObj, 0, &mapMode) < 0) { + cxoBuffer_clear(&nameBuffer); + cxoBuffer_clear(&metadataBuffer); + return NULL; + } + + // create collection + if (cxoConnection_getSodaFlags(db->connection, &flags) < 0) + return NULL; + if (mapMode) + flags |= DPI_SODA_FLAGS_CREATE_COLL_MAP; + Py_BEGIN_ALLOW_THREADS + status = dpiSodaDb_createCollection(db->handle, nameBuffer.ptr, + nameBuffer.size, metadataBuffer.ptr, metadataBuffer.size, flags, + &handle); + Py_END_ALLOW_THREADS + cxoBuffer_clear(&nameBuffer); + cxoBuffer_clear(&metadataBuffer); + if (status < 0) + return cxoError_raiseAndReturnNull(); + coll = cxoSodaCollection_new(db, handle); + if (!coll) { + dpiSodaColl_release(handle); + return NULL; + } + + return (PyObject*) coll; +} + + +//----------------------------------------------------------------------------- +// cxoSodaDatabase_createDocument() +// Create a SODA document with the specified key, content and media type. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaDatabase_createDocument(cxoSodaDatabase *db, + PyObject *args, PyObject *keywordArgs) +{ + static char *keywordList[] = { "content", "key", "mediaType", NULL }; + cxoBuffer contentBuffer, keyBuffer, mediaTypeBuffer; + PyObject *contentObj, *keyObj, *mediaTypeObj; + const char *encoding; + dpiSodaDoc *doc; + int status; + + // parse arguments + keyObj = mediaTypeObj = NULL; + if (!PyArg_ParseTupleAndKeywords(args, keywordArgs, "O|OO", keywordList, + &contentObj, &keyObj, &mediaTypeObj)) + return NULL; + + // content must be converted to string if it is a dictionary + if (PyDict_Check(contentObj)) { + contentObj = PyObject_CallFunctionObjArgs(cxoJsonDumpFunction, + contentObj, NULL); + if (!contentObj) + return NULL; + } + + // get buffers for each of the content, key and media type parameters + if (cxoUtils_processJsonArg(contentObj, &contentBuffer) < 0) + return NULL; + encoding = db->connection->encodingInfo.encoding; + if (cxoBuffer_fromObject(&keyBuffer, keyObj, encoding) < 0) { + cxoBuffer_clear(&contentBuffer); + return NULL; + } + if (cxoBuffer_fromObject(&mediaTypeBuffer, mediaTypeObj, encoding) < 0) { + cxoBuffer_clear(&contentBuffer); + cxoBuffer_clear(&keyBuffer); + return NULL; + } + + // create SODA document + status = dpiSodaDb_createDocument(db->handle, keyBuffer.ptr, + keyBuffer.size, contentBuffer.ptr, contentBuffer.size, + mediaTypeBuffer.ptr, mediaTypeBuffer.size, DPI_SODA_FLAGS_DEFAULT, + &doc); + cxoBuffer_clear(&contentBuffer); + cxoBuffer_clear(&keyBuffer); + cxoBuffer_clear(&mediaTypeBuffer); + if (status < 0) + return cxoError_raiseAndReturnNull(); + + return (PyObject*) cxoSodaDoc_new(db, doc); +} + + +//----------------------------------------------------------------------------- +// cxoSodaDatabase_getCollectionNames() +// Return a list of the names of the collections found in the database. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaDatabase_getCollectionNames(cxoSodaDatabase *db, + PyObject *args, PyObject *keywordArgs) +{ + static char *keywordList[] = { "startName", "limit", NULL }; + PyObject *startName, *result, *temp; + dpiSodaCollNames collNames; + cxoBuffer startNameBuffer; + uint32_t limit, i, flags; + const char *encoding; + int status; + + // parse arguments + limit = 0; + startName = NULL; + if (!PyArg_ParseTupleAndKeywords(args, keywordArgs, "|Oi", keywordList, + &startName, &limit)) + return NULL; + + // get collection names from the database + encoding = db->connection->encodingInfo.encoding; + if (cxoBuffer_fromObject(&startNameBuffer, startName, encoding) < 0) + return NULL; + if (cxoConnection_getSodaFlags(db->connection, &flags) < 0) + return NULL; + Py_BEGIN_ALLOW_THREADS + status = dpiSodaDb_getCollectionNames(db->handle, + (const char*) startNameBuffer.ptr, startNameBuffer.size, limit, + flags, &collNames); + Py_END_ALLOW_THREADS + cxoBuffer_clear(&startNameBuffer); + if (status < 0) + return cxoError_raiseAndReturnNull(); + + // transform results into a Python list + result = PyList_New(collNames.numNames); + if (!result) + return NULL; + for (i = 0; i < collNames.numNames; i++) { + temp = cxoPyString_fromEncodedString(collNames.names[i], + collNames.nameLengths[i], encoding, NULL); + if (!temp) { + Py_DECREF(result); + return NULL; + } + PyList_SET_ITEM(result, i, temp); + } + if (dpiSodaDb_freeCollectionNames(db->handle, &collNames) < 0) { + Py_DECREF(result); + return cxoError_raiseAndReturnNull(); + } + + return result; +} + + +//----------------------------------------------------------------------------- +// cxoSodaDatabase_openCollection() +// Open a SODA collection and return it. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaDatabase_openCollection(cxoSodaDatabase *db, + PyObject *nameObj) +{ + cxoSodaCollection *coll; + cxoBuffer nameBuffer; + dpiSodaColl *handle; + uint32_t flags; + int status; + + // open collection + if (cxoBuffer_fromObject(&nameBuffer, nameObj, + db->connection->encodingInfo.encoding) < 0) + return NULL; + if (cxoConnection_getSodaFlags(db->connection, &flags) < 0) + return NULL; + Py_BEGIN_ALLOW_THREADS + status = dpiSodaDb_openCollection(db->handle, nameBuffer.ptr, + nameBuffer.size, flags, &handle); + Py_END_ALLOW_THREADS + cxoBuffer_clear(&nameBuffer); + if (status < 0) + return cxoError_raiseAndReturnNull(); + if (!handle) + Py_RETURN_NONE; + coll = cxoSodaCollection_new(db, handle); + if (!coll) { + dpiSodaColl_release(handle); + return NULL; + } + + return (PyObject*) coll; +} + diff --git a/src/cxoSodaDoc.c b/src/cxoSodaDoc.c new file mode 100644 index 0000000..8ea967a --- /dev/null +++ b/src/cxoSodaDoc.c @@ -0,0 +1,309 @@ +//----------------------------------------------------------------------------- +// Copyright 2018, Oracle and/or its affiliates. All rights reserved. +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// cxoSodaDoc.c +// Defines the routines for handling SODA documents. +//----------------------------------------------------------------------------- + +#include "cxoModule.h" + +//----------------------------------------------------------------------------- +// Declaration of functions +//----------------------------------------------------------------------------- +static void cxoSodaDoc_free(cxoSodaDoc*); +static PyObject *cxoSodaDoc_repr(cxoSodaDoc*); +static PyObject *cxoSodaDoc_getCreatedOn(cxoSodaDoc*, void*); +static PyObject *cxoSodaDoc_getKey(cxoSodaDoc*, void*); +static PyObject *cxoSodaDoc_getLastModified(cxoSodaDoc*, void*); +static PyObject *cxoSodaDoc_getMediaType(cxoSodaDoc*, void*); +static PyObject *cxoSodaDoc_getVersion(cxoSodaDoc*, void*); +static PyObject *cxoSodaDoc_getContent(cxoSodaDoc*, PyObject*); +static PyObject *cxoSodaDoc_getContentAsBytes(cxoSodaDoc*, PyObject*); +static PyObject *cxoSodaDoc_getContentAsString(cxoSodaDoc*, PyObject*); + + +//----------------------------------------------------------------------------- +// declaration of methods +//----------------------------------------------------------------------------- +static PyMethodDef cxoMethods[] = { + { "getContent", (PyCFunction) cxoSodaDoc_getContent, METH_NOARGS }, + { "getContentAsBytes", (PyCFunction) cxoSodaDoc_getContentAsBytes, + METH_NOARGS }, + { "getContentAsString", (PyCFunction) cxoSodaDoc_getContentAsString, + METH_NOARGS }, + { NULL } +}; + + +//----------------------------------------------------------------------------- +// declaration of calculated members +//----------------------------------------------------------------------------- +static PyGetSetDef cxoCalcMembers[] = { + { "createdOn", (getter) cxoSodaDoc_getCreatedOn, 0, 0, 0 }, + { "key", (getter) cxoSodaDoc_getKey, 0, 0, 0 }, + { "lastModified", (getter) cxoSodaDoc_getLastModified, 0, 0, 0 }, + { "mediaType", (getter) cxoSodaDoc_getMediaType, 0, 0, 0 }, + { "version", (getter) cxoSodaDoc_getVersion, 0, 0, 0 }, + { NULL } +}; + + +//----------------------------------------------------------------------------- +// Python type declarations +//----------------------------------------------------------------------------- +PyTypeObject cxoPyTypeSodaDoc = { + PyVarObject_HEAD_INIT(NULL, 0) + "cx_Oracle.SodaDoc", // tp_name + sizeof(cxoSodaDoc), // tp_basicsize + 0, // tp_itemsize + (destructor) cxoSodaDoc_free, // tp_dealloc + 0, // tp_print + 0, // tp_getattr + 0, // tp_setattr + 0, // tp_compare + (reprfunc) cxoSodaDoc_repr, // tp_repr + 0, // tp_as_number + 0, // tp_as_sequence + 0, // tp_as_mapping + 0, // tp_hash + 0, // tp_call + 0, // tp_str + 0, // tp_getattro + 0, // tp_setattro + 0, // tp_as_buffer + Py_TPFLAGS_DEFAULT, // tp_flags + 0, // tp_doc + 0, // tp_traverse + 0, // tp_clear + 0, // tp_richcompare + 0, // tp_weaklistoffset + 0, // tp_iter + 0, // tp_iternext + cxoMethods, // tp_methods + 0, // tp_members + cxoCalcMembers, // tp_getset + 0, // tp_base + 0, // tp_dict + 0, // tp_descr_get + 0, // tp_descr_set + 0, // tp_dictoffset + 0, // tp_init + 0, // tp_alloc + 0, // tp_new + 0, // tp_free + 0, // tp_is_gc + 0 // tp_bases +}; + + +//----------------------------------------------------------------------------- +// cxoSodaDoc_new() +// Create a new SODA document. +//----------------------------------------------------------------------------- +cxoSodaDoc *cxoSodaDoc_new(cxoSodaDatabase *db, dpiSodaDoc *handle) +{ + cxoSodaDoc *doc; + + doc = (cxoSodaDoc*) cxoPyTypeSodaDoc.tp_alloc(&cxoPyTypeSodaDoc, 0); + if (!doc) { + dpiSodaDoc_release(handle); + return NULL; + } + Py_INCREF(db); + doc->db = db; + doc->handle = handle; + return doc; +} + + +//----------------------------------------------------------------------------- +// cxoSodaDoc_free() +// Free the memory associated with a SODA document. +//----------------------------------------------------------------------------- +static void cxoSodaDoc_free(cxoSodaDoc *doc) +{ + if (doc->handle) { + dpiSodaDoc_release(doc->handle); + doc->handle = NULL; + } + Py_CLEAR(doc->db); + Py_TYPE(doc)->tp_free((PyObject*) doc); +} + + +//----------------------------------------------------------------------------- +// cxoSodaDoc_repr() +// Return a string representation of a SODA document. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaDoc_repr(cxoSodaDoc *doc) +{ + PyObject *module, *name, *result, *keyObj; + uint32_t keyLength; + const char *key; + + if (dpiSodaDoc_getKey(doc->handle, &key, &keyLength) < 0) + return cxoError_raiseAndReturnNull(); + keyObj = cxoPyString_fromEncodedString(key, keyLength, + doc->db->connection->encodingInfo.encoding, NULL); + if (!keyObj) + return NULL; + if (cxoUtils_getModuleAndName(Py_TYPE(doc), &module, &name) < 0) { + Py_DECREF(keyObj); + return NULL; + } + result = cxoUtils_formatString("<%s.%s with key %s>", + PyTuple_Pack(3, module, name, keyObj)); + Py_DECREF(module); + Py_DECREF(name); + return result; +} + + +//----------------------------------------------------------------------------- +// cxoSodaDoc_getCreatedOn() +// Retrieve the time the SODA document was created, as a string in ISO 8601 +// format. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaDoc_getCreatedOn(cxoSodaDoc *doc, void *unused) +{ + uint32_t valueLength; + const char *value; + + if (dpiSodaDoc_getCreatedOn(doc->handle, &value, &valueLength) < 0) + return cxoError_raiseAndReturnNull(); + if (valueLength > 0) + return cxoPyString_fromEncodedString(value, valueLength, + doc->db->connection->encodingInfo.encoding, NULL); + Py_RETURN_NONE; +} + + +//----------------------------------------------------------------------------- +// cxoSodaDoc_getKey() +// Retrieve the key for the SODA document. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaDoc_getKey(cxoSodaDoc *doc, void *unused) +{ + uint32_t valueLength; + const char *value; + + if (dpiSodaDoc_getKey(doc->handle, &value, &valueLength) < 0) + return cxoError_raiseAndReturnNull(); + if (valueLength > 0) + return cxoPyString_fromEncodedString(value, valueLength, + doc->db->connection->encodingInfo.encoding, NULL); + Py_RETURN_NONE; +} + + +//----------------------------------------------------------------------------- +// cxoSodaDoc_getLastModified() +// Retrieve the time the SODA document was last modified, as a string in ISO +// 8601 format. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaDoc_getLastModified(cxoSodaDoc *doc, void *unused) +{ + uint32_t valueLength; + const char *value; + + if (dpiSodaDoc_getLastModified(doc->handle, &value, &valueLength) < 0) + return cxoError_raiseAndReturnNull(); + if (valueLength > 0) + return cxoPyString_fromEncodedString(value, valueLength, + doc->db->connection->encodingInfo.encoding, NULL); + Py_RETURN_NONE; +} + + +//----------------------------------------------------------------------------- +// cxoSodaDoc_getMediaType() +// Retrieve the media type of the SODA document. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaDoc_getMediaType(cxoSodaDoc *doc, void *unused) +{ + uint32_t valueLength; + const char *value; + + if (dpiSodaDoc_getMediaType(doc->handle, &value, &valueLength) < 0) + return cxoError_raiseAndReturnNull(); + if (valueLength > 0) + return cxoPyString_fromEncodedString(value, valueLength, + doc->db->connection->encodingInfo.encoding, NULL); + Py_RETURN_NONE; +} + + +//----------------------------------------------------------------------------- +// cxoSodaDoc_getVersion() +// Retrieve the version for the SODA document. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaDoc_getVersion(cxoSodaDoc *doc, void *unused) +{ + uint32_t valueLength; + const char *value; + + if (dpiSodaDoc_getVersion(doc->handle, &value, &valueLength) < 0) + return cxoError_raiseAndReturnNull(); + if (valueLength > 0) + return cxoPyString_fromEncodedString(value, valueLength, + doc->db->connection->encodingInfo.encoding, NULL); + Py_RETURN_NONE; +} + + +//----------------------------------------------------------------------------- +// cxoSodaDoc_getContent() +// Get the content from the document and return a Python object. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaDoc_getContent(cxoSodaDoc *doc, PyObject *args) +{ + PyObject *str, *result; + + str = cxoSodaDoc_getContentAsString(doc, args); + if (!str) + return NULL; + if (str == Py_None) + return str; + result = PyObject_CallFunctionObjArgs(cxoJsonLoadFunction, str, NULL); + Py_DECREF(str); + return result; +} + + +//----------------------------------------------------------------------------- +// cxoSodaDoc_getContentAsBytes() +// Get the content from the document and return a bytes object. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaDoc_getContentAsBytes(cxoSodaDoc *doc, PyObject *args) +{ + const char *content, *encoding; + uint32_t contentLength; + + if (dpiSodaDoc_getContent(doc->handle, &content, &contentLength, + &encoding) < 0) + return cxoError_raiseAndReturnNull(); + if (contentLength > 0) + return PyBytes_FromStringAndSize(content, contentLength); + Py_RETURN_NONE; +} + + +//----------------------------------------------------------------------------- +// cxoSodaDoc_getContentAsString() +// Get the content from the document and return a string. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaDoc_getContentAsString(cxoSodaDoc *doc, PyObject *args) +{ + const char *content, *encoding; + uint32_t contentLength; + + if (dpiSodaDoc_getContent(doc->handle, &content, &contentLength, + &encoding) < 0) + return cxoError_raiseAndReturnNull(); + if (contentLength > 0) + return PyUnicode_Decode(content, contentLength, encoding, NULL); + Py_RETURN_NONE; +} + diff --git a/src/cxoSodaDocCursor.c b/src/cxoSodaDocCursor.c new file mode 100644 index 0000000..1265deb --- /dev/null +++ b/src/cxoSodaDocCursor.c @@ -0,0 +1,184 @@ +//----------------------------------------------------------------------------- +// Copyright 2018, Oracle and/or its affiliates. All rights reserved. +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// cxoSodaDocCursor.c +// Defines the routines for handling SODA document cursors. These cursors +// permit iterating over the documents that match the criteria that was +// specified by the user. +//----------------------------------------------------------------------------- + +#include "cxoModule.h" + +//----------------------------------------------------------------------------- +// Declaration of functions +//----------------------------------------------------------------------------- +static void cxoSodaDocCursor_free(cxoSodaDocCursor*); +static PyObject *cxoSodaDocCursor_repr(cxoSodaDocCursor*); +static PyObject *cxoSodaDocCursor_getIter(cxoSodaDocCursor*); +static PyObject *cxoSodaDocCursor_getNext(cxoSodaDocCursor*); +static PyObject *cxoSodaDocCursor_close(cxoSodaDocCursor*, PyObject*); + + +//----------------------------------------------------------------------------- +// declaration of methods +//----------------------------------------------------------------------------- +static PyMethodDef cxoMethods[] = { + { "close", (PyCFunction) cxoSodaDocCursor_close, METH_NOARGS }, + { NULL } +}; + + +//----------------------------------------------------------------------------- +// Python type declarations +//----------------------------------------------------------------------------- +PyTypeObject cxoPyTypeSodaDocCursor = { + PyVarObject_HEAD_INIT(NULL, 0) + "cx_Oracle.SodaDocCursor", // tp_name + sizeof(cxoSodaDocCursor), // tp_basicsize + 0, // tp_itemsize + (destructor) cxoSodaDocCursor_free, // tp_dealloc + 0, // tp_print + 0, // tp_getattr + 0, // tp_setattr + 0, // tp_compare + (reprfunc) cxoSodaDocCursor_repr, // tp_repr + 0, // tp_as_number + 0, // tp_as_sequence + 0, // tp_as_mapping + 0, // tp_hash + 0, // tp_call + 0, // tp_str + 0, // tp_getattro + 0, // tp_setattro + 0, // tp_as_buffer + Py_TPFLAGS_DEFAULT, // tp_flags + 0, // tp_doc + 0, // tp_traverse + 0, // tp_clear + 0, // tp_richcompare + 0, // tp_weaklistoffset + (getiterfunc) cxoSodaDocCursor_getIter, // tp_iter + (iternextfunc) cxoSodaDocCursor_getNext, // tp_iternext + cxoMethods, // tp_methods + 0, // tp_members + 0, // tp_getset + 0, // tp_base + 0, // tp_dict + 0, // tp_descr_get + 0, // tp_descr_set + 0, // tp_dictoffset + 0, // tp_init + 0, // tp_alloc + 0, // tp_new + 0, // tp_free + 0, // tp_is_gc + 0 // tp_bases +}; + + +//----------------------------------------------------------------------------- +// cxoSodaDocCursor_new() +// Create a new SODA document cursor. +//----------------------------------------------------------------------------- +cxoSodaDocCursor *cxoSodaDocCursor_new(cxoSodaDatabase *db, + dpiSodaDocCursor *handle) +{ + cxoSodaDocCursor *cursor; + + cursor = (cxoSodaDocCursor*) + cxoPyTypeSodaDocCursor.tp_alloc(&cxoPyTypeSodaDocCursor, 0); + if (!cursor) { + dpiSodaDocCursor_release(handle); + return NULL; + } + Py_INCREF(db); + cursor->db = db; + cursor->handle = handle; + return cursor; +} + + +//----------------------------------------------------------------------------- +// cxoSodaDocCursor_free() +// Free the memory associated with a SODA document cursor. +//----------------------------------------------------------------------------- +static void cxoSodaDocCursor_free(cxoSodaDocCursor *cursor) +{ + if (cursor->handle) { + dpiSodaDocCursor_release(cursor->handle); + cursor->handle = NULL; + } + Py_CLEAR(cursor->db); + Py_TYPE(cursor)->tp_free((PyObject*) cursor); +} + + +//----------------------------------------------------------------------------- +// cxoSodaDocCursor_repr() +// Return a string representation of a SODA document cursor. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaDocCursor_repr(cxoSodaDocCursor *cursor) +{ + PyObject *module, *name, *result; + + if (cxoUtils_getModuleAndName(Py_TYPE(cursor), &module, &name) < 0) + return NULL; + result = cxoUtils_formatString("<%s.%s>", PyTuple_Pack(2, module, name)); + Py_DECREF(module); + Py_DECREF(name); + return result; +} + + +//----------------------------------------------------------------------------- +// cxoSodaDocCursor_close() +// Create a SODA collection and return it. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaDocCursor_close(cxoSodaDocCursor *cursor, + PyObject *args) +{ + if (dpiSodaDocCursor_close(cursor->handle) < 0) + return cxoError_raiseAndReturnNull(); + Py_RETURN_NONE; +} + + +//----------------------------------------------------------------------------- +// cxoSodaDocCursor_getIter() +// Return a reference to the cursor which supports the iterator protocol. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaDocCursor_getIter(cxoSodaDocCursor *cursor) +{ + Py_INCREF(cursor); + return (PyObject*) cursor; +} + + +//----------------------------------------------------------------------------- +// cxoSodaDocCursor_getNext() +// Return the next document from the cursor. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaDocCursor_getNext(cxoSodaDocCursor *cursor) +{ + dpiSodaDoc *handle; + cxoSodaDoc *doc; + uint32_t flags; + int status; + + if (cxoConnection_getSodaFlags(cursor->db->connection, &flags) < 0) + return NULL; + Py_BEGIN_ALLOW_THREADS + status = dpiSodaDocCursor_getNext(cursor->handle, flags, &handle); + Py_END_ALLOW_THREADS + if (status < 0) + return cxoError_raiseAndReturnNull(); + if (!handle) + return NULL; + doc = cxoSodaDoc_new(cursor->db, handle); + if (!doc) + return NULL; + return (PyObject*) doc; +} + diff --git a/src/cxoSodaOperation.c b/src/cxoSodaOperation.c new file mode 100644 index 0000000..78f5916 --- /dev/null +++ b/src/cxoSodaOperation.c @@ -0,0 +1,564 @@ +//----------------------------------------------------------------------------- +// Copyright 2018, Oracle and/or its affiliates. All rights reserved. +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// cxoSodaOperation.c +// Defines the routines for the various operations performed on SODA +// collections. +//----------------------------------------------------------------------------- + +#include "cxoModule.h" + +//----------------------------------------------------------------------------- +// Declaration of functions +//----------------------------------------------------------------------------- +static void cxoSodaOperation_free(cxoSodaOperation*); +static PyObject *cxoSodaOperation_repr(cxoSodaOperation*); +static PyObject *cxoSodaOperation_filter(cxoSodaOperation*, PyObject*); +static PyObject *cxoSodaOperation_key(cxoSodaOperation*, PyObject*); +static PyObject *cxoSodaOperation_keys(cxoSodaOperation*, PyObject*); +static PyObject *cxoSodaOperation_limit(cxoSodaOperation*, PyObject*); +static PyObject *cxoSodaOperation_skip(cxoSodaOperation*, PyObject*); +static PyObject *cxoSodaOperation_version(cxoSodaOperation*, PyObject*); +static PyObject *cxoSodaOperation_count(cxoSodaOperation*, PyObject*); +static PyObject *cxoSodaOperation_getCursor(cxoSodaOperation*, PyObject*); +static PyObject *cxoSodaOperation_getDocuments(cxoSodaOperation*, PyObject*); +static PyObject *cxoSodaOperation_getOne(cxoSodaOperation*, PyObject*); +static PyObject *cxoSodaOperation_remove(cxoSodaOperation*, PyObject*); +static PyObject *cxoSodaOperation_replaceOne(cxoSodaOperation*, PyObject*); +static PyObject *cxoSodaOperation_replaceOneAndGet(cxoSodaOperation*, + PyObject*); + + +//----------------------------------------------------------------------------- +// declaration of methods for Python type "SodaOperation" +//----------------------------------------------------------------------------- +static PyMethodDef cxoMethods[] = { + { "filter", (PyCFunction) cxoSodaOperation_filter, METH_O }, + { "key", (PyCFunction) cxoSodaOperation_key, METH_O }, + { "keys", (PyCFunction) cxoSodaOperation_keys, METH_O }, + { "limit", (PyCFunction) cxoSodaOperation_limit, METH_O }, + { "skip", (PyCFunction) cxoSodaOperation_skip, METH_O }, + { "version", (PyCFunction) cxoSodaOperation_version, METH_O }, + { "count", (PyCFunction) cxoSodaOperation_count, METH_NOARGS }, + { "getCursor", (PyCFunction) cxoSodaOperation_getCursor, METH_NOARGS }, + { "getDocuments", (PyCFunction) cxoSodaOperation_getDocuments, + METH_NOARGS }, + { "getOne", (PyCFunction) cxoSodaOperation_getOne, METH_NOARGS }, + { "remove", (PyCFunction) cxoSodaOperation_remove, METH_NOARGS }, + { "replaceOne", (PyCFunction) cxoSodaOperation_replaceOne, METH_O }, + { "replaceOneAndGet", (PyCFunction) cxoSodaOperation_replaceOneAndGet, + METH_O }, + { NULL } +}; + + +//----------------------------------------------------------------------------- +// Python type declarations +//----------------------------------------------------------------------------- +PyTypeObject cxoPyTypeSodaOperation = { + PyVarObject_HEAD_INIT(NULL, 0) + "cx_Oracle.SodaOperation", // tp_name + sizeof(cxoSodaOperation), // tp_basicsize + 0, // tp_itemsize + (destructor) cxoSodaOperation_free, // tp_dealloc + 0, // tp_print + 0, // tp_getattr + 0, // tp_setattr + 0, // tp_compare + (reprfunc) cxoSodaOperation_repr, // tp_repr + 0, // tp_as_number + 0, // tp_as_sequence + 0, // tp_as_mapping + 0, // tp_hash + 0, // tp_call + 0, // tp_str + 0, // tp_getattro + 0, // tp_setattro + 0, // tp_as_buffer + Py_TPFLAGS_DEFAULT, // tp_flags + 0, // tp_doc + 0, // tp_traverse + 0, // tp_clear + 0, // tp_richcompare + 0, // tp_weaklistoffset + 0, // tp_iter + 0, // tp_iternext + cxoMethods, // tp_methods + 0, // tp_members + 0, // tp_getset + 0, // tp_base + 0, // tp_dict + 0, // tp_descr_get + 0, // tp_descr_set + 0, // tp_dictoffset + 0, // tp_init + 0, // tp_alloc + 0, // tp_new + 0, // tp_free + 0, // tp_is_gc + 0 // tp_bases +}; + + +//----------------------------------------------------------------------------- +// cxoSodaOperation_clearKeys() +// Clear the keys set on the operation object, if applicable. +//----------------------------------------------------------------------------- +void cxoSodaOperation_clearKeys(cxoSodaOperation *op) +{ + uint32_t i; + + if (op->keyBuffers) { + for (i = 0; i < op->numKeyBuffers; i++) + cxoBuffer_clear(&op->keyBuffers[i]); + PyMem_Free(op->keyBuffers); + op->keyBuffers = NULL; + } + op->numKeyBuffers = 0; + op->options.numKeys = 0; + if (op->options.keys) { + PyMem_Free(op->options.keys); + op->options.keys = NULL; + } + if (op->options.keyLengths) { + PyMem_Free(op->options.keyLengths); + op->options.keyLengths = NULL; + } +} + + +//----------------------------------------------------------------------------- +// cxoSodaOperation_new() +// Create a new SODA operation object. +//----------------------------------------------------------------------------- +cxoSodaOperation *cxoSodaOperation_new(cxoSodaCollection *coll) +{ + cxoSodaOperation *op; + + op = (cxoSodaOperation*) + cxoPyTypeSodaOperation.tp_alloc(&cxoPyTypeSodaOperation, 0); + if (!op) + return NULL; + if (dpiContext_initSodaOperOptions(cxoDpiContext, &op->options) < 0) { + Py_DECREF(op); + return NULL; + } + cxoBuffer_init(&op->keyBuffer); + cxoBuffer_init(&op->versionBuffer); + cxoBuffer_init(&op->filterBuffer); + Py_INCREF(coll); + op->coll = coll; + + return op; +} + + +//----------------------------------------------------------------------------- +// cxoSodaOperation_free() +// Free the memory associated with a SODA operation object. +//----------------------------------------------------------------------------- +static void cxoSodaOperation_free(cxoSodaOperation *op) +{ + cxoSodaOperation_clearKeys(op); + cxoBuffer_clear(&op->keyBuffer); + cxoBuffer_clear(&op->versionBuffer); + cxoBuffer_clear(&op->filterBuffer); + Py_CLEAR(op->coll); + Py_TYPE(op)->tp_free((PyObject*) op); +} + + +//----------------------------------------------------------------------------- +// cxoSodaOperation_repr() +// Return a string representation of a SODA operation object. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaOperation_repr(cxoSodaOperation *op) +{ + PyObject *collRepr, *module, *name, *result; + + collRepr = PyObject_Repr((PyObject*) op->coll); + if (!collRepr) + return NULL; + if (cxoUtils_getModuleAndName(Py_TYPE(op), &module, &name) < 0) { + Py_DECREF(collRepr); + return NULL; + } + result = cxoUtils_formatString("<%s.%s on %s>", + PyTuple_Pack(3, module, name, collRepr)); + Py_DECREF(module); + Py_DECREF(name); + Py_DECREF(collRepr); + return result; +} + + +//----------------------------------------------------------------------------- +// cxoSodaOperation_filter() +// Set the filter to be used for the operation. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaOperation_filter(cxoSodaOperation *op, + PyObject *filterObj) +{ + cxoBuffer_clear(&op->filterBuffer); + if (PyDict_Check(filterObj)) { + filterObj = PyObject_CallFunctionObjArgs(cxoJsonDumpFunction, + filterObj, NULL); + if (!filterObj) + return NULL; + } + if (cxoBuffer_fromObject(&op->filterBuffer, filterObj, + op->coll->db->connection->encodingInfo.encoding) < 0) + return NULL; + op->options.filter = op->filterBuffer.ptr; + op->options.filterLength = op->filterBuffer.size; + Py_INCREF(op); + return (PyObject*) op; +} + + +//----------------------------------------------------------------------------- +// cxoSodaOperation_key() +// Set the key to be used for the operation. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaOperation_key(cxoSodaOperation *op, + PyObject *keyObj) +{ + cxoBuffer_clear(&op->keyBuffer); + if (cxoBuffer_fromObject(&op->keyBuffer, keyObj, + op->coll->db->connection->encodingInfo.encoding) < 0) + return NULL; + op->options.key = op->keyBuffer.ptr; + op->options.keyLength = op->keyBuffer.size; + Py_INCREF(op); + return (PyObject*) op; +} + + +//----------------------------------------------------------------------------- +// cxoSodaOperation_keys() +// Set the keys to be used for the operation. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaOperation_keys(cxoSodaOperation *op, + PyObject *keysObj) +{ + Py_ssize_t size, i; + PyObject *element; + + // determine size of sequence passed to method + size = PySequence_Size(keysObj); + if (PyErr_Occurred()) + return NULL; + + // clear original keys, if applicable + cxoSodaOperation_clearKeys(op); + + // zero-length arrays don't need any further processing + if (size == 0) { + Py_INCREF(op); + return (PyObject*) op; + } + + // initialize memory + op->keyBuffers = PyMem_Malloc(size * sizeof(cxoBuffer)); + if (!op->keyBuffers) + return NULL; + op->numKeyBuffers = (uint32_t) size; + for (i = 0; i < size; i++) + cxoBuffer_init(&op->keyBuffers[i]); + op->options.keys = PyMem_Malloc(size * sizeof(const char *)); + op->options.keyLengths = PyMem_Malloc(size * sizeof(uint32_t)); + if (!op->options.keys || !op->options.keyLengths) { + cxoSodaOperation_clearKeys(op); + return NULL; + } + op->options.numKeys = op->numKeyBuffers; + + // process each of the elements of the sequence + for (i = 0; i < size; i++) { + element = PySequence_GetItem(keysObj, i); + if (!element) { + cxoSodaOperation_clearKeys(op); + return NULL; + } + if (cxoBuffer_fromObject(&op->keyBuffers[i], element, + op->coll->db->connection->encodingInfo.encoding) < 0) { + Py_DECREF(element); + cxoSodaOperation_clearKeys(op); + return NULL; + } + Py_DECREF(element); + op->options.keys[i] = op->keyBuffers[i].ptr; + op->options.keyLengths[i] = op->keyBuffers[i].size; + } + + Py_INCREF(op); + return (PyObject*) op; +} + + +//----------------------------------------------------------------------------- +// cxoSodaOperation_limit() +// Set the limit value to be used for the operation. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaOperation_limit(cxoSodaOperation *op, + PyObject *limitObj) +{ + op->options.limit = PyLong_AsUnsignedLong(limitObj); + if (PyErr_Occurred()) + return NULL; + Py_INCREF(op); + return (PyObject*) op; +} + + +//----------------------------------------------------------------------------- +// cxoSodaOperation_skip() +// Set the skip value to be used for the operation. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaOperation_skip(cxoSodaOperation *op, + PyObject *skipObj) +{ + op->options.skip = PyLong_AsUnsignedLong(skipObj); + if (PyErr_Occurred()) + return NULL; + Py_INCREF(op); + return (PyObject*) op; +} + + +//----------------------------------------------------------------------------- +// cxoSodaOperation_version() +// Set the version to be used for the operation. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaOperation_version(cxoSodaOperation *op, + PyObject *versionObj) +{ + cxoBuffer_clear(&op->versionBuffer); + if (cxoBuffer_fromObject(&op->versionBuffer, versionObj, + op->coll->db->connection->encodingInfo.encoding) < 0) + return NULL; + op->options.version = op->versionBuffer.ptr; + op->options.versionLength = op->versionBuffer.size; + Py_INCREF(op); + return (PyObject*) op; +} + + +//----------------------------------------------------------------------------- +// cxoSodaOperation_count() +// Returns the number of documents that match the criteria. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaOperation_count(cxoSodaOperation *op, PyObject *args) +{ + uint64_t count; + uint32_t flags; + int status; + + if (cxoConnection_getSodaFlags(op->coll->db->connection, &flags) < 0) + return NULL; + Py_BEGIN_ALLOW_THREADS + status = dpiSodaColl_getDocCount(op->coll->handle, &op->options, flags, + &count); + Py_END_ALLOW_THREADS + if (status < 0) + return cxoError_raiseAndReturnNull(); + return PyLong_FromUnsignedLongLong(count); +} + + +//----------------------------------------------------------------------------- +// cxoSodaOperation_getCursor() +// Returns a document cursor which can be used to iterate over the documents +// that match the criteria. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaOperation_getCursor(cxoSodaOperation *op, + PyObject *args) +{ + dpiSodaDocCursor *handle; + cxoSodaDocCursor *cursor; + uint32_t flags; + int status; + + if (cxoConnection_getSodaFlags(op->coll->db->connection, &flags) < 0) + return NULL; + Py_BEGIN_ALLOW_THREADS + status = dpiSodaColl_find(op->coll->handle, &op->options, flags, &handle); + Py_END_ALLOW_THREADS + if (status < 0) + return cxoError_raiseAndReturnNull(); + cursor = cxoSodaDocCursor_new(op->coll->db, handle); + if (!cursor) + return NULL; + return (PyObject*) cursor; +} + + +//----------------------------------------------------------------------------- +// cxoSodaOperation_getDocuments() +// Returns a list of documents that match the criteria. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaOperation_getDocuments(cxoSodaOperation *op, + PyObject *args) +{ + dpiSodaDocCursor *cursor; + PyObject *docObj; + dpiSodaDoc *doc; + PyObject *list; + uint32_t flags; + int status; + + // acquire cursor + if (cxoConnection_getSodaFlags(op->coll->db->connection, &flags) < 0) + return NULL; + Py_BEGIN_ALLOW_THREADS + status = dpiSodaColl_find(op->coll->handle, &op->options, flags, &cursor); + Py_END_ALLOW_THREADS + if (status < 0) + return cxoError_raiseAndReturnNull(); + + // iterate cursor and create array of documents + list = PyList_New(0); + if (!list) { + dpiSodaDocCursor_release(cursor); + return NULL; + } + while (1) { + Py_BEGIN_ALLOW_THREADS + status = dpiSodaDocCursor_getNext(cursor, flags, &doc); + Py_END_ALLOW_THREADS + if (status < 0) { + cxoError_raiseAndReturnNull(); + dpiSodaDocCursor_release(cursor); + return NULL; + } + if (!doc) + break; + docObj = (PyObject*) cxoSodaDoc_new(op->coll->db, doc); + if (!docObj) { + dpiSodaDocCursor_release(cursor); + return NULL; + } + if (PyList_Append(list, docObj) < 0) { + Py_DECREF(docObj); + dpiSodaDocCursor_release(cursor); + return NULL; + } + Py_DECREF(docObj); + } + dpiSodaDocCursor_release(cursor); + + return list; +} + + +//----------------------------------------------------------------------------- +// cxoSodaOperation_getOne() +// Returns a single document that matches the criteria or None if no +// documents match the criteria. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaOperation_getOne(cxoSodaOperation *op, PyObject *args) +{ + dpiSodaDoc *handle; + uint32_t flags; + int status; + + if (cxoConnection_getSodaFlags(op->coll->db->connection, &flags) < 0) + return NULL; + Py_BEGIN_ALLOW_THREADS + status = dpiSodaColl_findOne(op->coll->handle, &op->options, flags, + &handle); + Py_END_ALLOW_THREADS + if (status < 0) + return cxoError_raiseAndReturnNull(); + if (handle) + return (PyObject*) cxoSodaDoc_new(op->coll->db, handle); + Py_RETURN_NONE; +} + + +//----------------------------------------------------------------------------- +// cxoSodaOperation_remove() +// Remove all of the documents that match the criteria. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaOperation_remove(cxoSodaOperation *op, PyObject *args) +{ + uint64_t count; + uint32_t flags; + int status; + + if (cxoConnection_getSodaFlags(op->coll->db->connection, &flags) < 0) + return NULL; + Py_BEGIN_ALLOW_THREADS + status = dpiSodaColl_remove(op->coll->handle, &op->options, flags, &count); + Py_END_ALLOW_THREADS + if (status < 0) + return cxoError_raiseAndReturnNull(); + return PyLong_FromUnsignedLongLong(count); +} + + +//----------------------------------------------------------------------------- +// cxoSodaOperation_replaceOne() +// Replace a single document in the collection with the provided replacement. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaOperation_replaceOne(cxoSodaOperation *op, + PyObject *arg) +{ + int status, replaced; + cxoSodaDoc *doc; + uint32_t flags; + + if (cxoConnection_getSodaFlags(op->coll->db->connection, &flags) < 0) + return NULL; + if (cxoUtils_processSodaDocArg(op->coll->db, arg, &doc) < 0) + return NULL; + Py_BEGIN_ALLOW_THREADS + status = dpiSodaColl_replaceOne(op->coll->handle, &op->options, + doc->handle, flags, &replaced, NULL); + Py_END_ALLOW_THREADS + if (status < 0) { + cxoError_raiseAndReturnNull(); + Py_DECREF(doc); + return NULL; + } + Py_DECREF(doc); + if (replaced) + Py_RETURN_TRUE; + Py_RETURN_FALSE; +} + + +//----------------------------------------------------------------------------- +// cxoSodaOperation_replaceOneAndGet() +// Replace a single document in the collection with the provided replacement +// and return a document (without the content) to the caller. +//----------------------------------------------------------------------------- +static PyObject *cxoSodaOperation_replaceOneAndGet(cxoSodaOperation *op, + PyObject *arg) +{ + dpiSodaDoc *replacedDoc; + cxoSodaDoc *doc; + uint32_t flags; + int status; + + if (cxoConnection_getSodaFlags(op->coll->db->connection, &flags) < 0) + return NULL; + if (cxoUtils_processSodaDocArg(op->coll->db, arg, &doc) < 0) + return NULL; + Py_BEGIN_ALLOW_THREADS + status = dpiSodaColl_replaceOne(op->coll->handle, &op->options, + doc->handle, flags, NULL, &replacedDoc); + Py_END_ALLOW_THREADS + if (status < 0) { + cxoError_raiseAndReturnNull(); + Py_DECREF(doc); + return NULL; + } + Py_DECREF(doc); + if (replacedDoc) + return (PyObject*) cxoSodaDoc_new(op->coll->db, replacedDoc); + Py_RETURN_NONE; +} + diff --git a/src/cxoUtils.c b/src/cxoUtils.c index c9af0e8..a0b7ee5 100644 --- a/src/cxoUtils.c +++ b/src/cxoUtils.c @@ -131,3 +131,73 @@ int cxoUtils_initializeDPI(void) return 0; } + +//----------------------------------------------------------------------------- +// cxoUtils_processJsonArg() +// Process the argument which is expected to be either a string or bytes, or +// a dictionary or list which is converted to a string via the json.dumps() +// method. All strings are encoded to UTF-8 which is what SODA expects. +//----------------------------------------------------------------------------- +int cxoUtils_processJsonArg(PyObject *arg, cxoBuffer *buffer) +{ + int converted = 0; + + if (arg && (PyDict_Check(arg) || PyList_Check(arg))) { + arg = PyObject_CallFunctionObjArgs(cxoJsonDumpFunction, arg, NULL); + if (!arg) + return -1; + converted = 1; + } + if (cxoBuffer_fromObject(buffer, arg, "UTF-8") < 0) + return -1; + if (converted) + Py_DECREF(arg); + + return 0; +} + + +//----------------------------------------------------------------------------- +// cxoUtils_processSodaDocArg() +// Process a SODA document argument. This is expectd to be an actual SODA +// document object or a dictionary. If the argument refers to a dictionary or +// list, a new SODA document will be created with the given content and without +// a key or media type specified. +//----------------------------------------------------------------------------- +int cxoUtils_processSodaDocArg(cxoSodaDatabase *db, PyObject *arg, + cxoSodaDoc **doc) +{ + dpiSodaDoc *handle; + cxoBuffer buffer; + + if (PyObject_TypeCheck(arg, &cxoPyTypeSodaDoc)) { + Py_INCREF(arg); + *doc = (cxoSodaDoc*) arg; + } else if (PyDict_Check(arg) || PyList_Check(arg)) { + arg = PyObject_CallFunctionObjArgs(cxoJsonDumpFunction, arg, NULL); + if (!arg) + return -1; + if (cxoBuffer_fromObject(&buffer, arg, "UTF-8") < 0) { + Py_DECREF(arg); + return -1; + } + Py_DECREF(arg); + if (dpiSodaDb_createDocument(db->handle, NULL, 0, buffer.ptr, + buffer.size, NULL, 0, DPI_SODA_FLAGS_DEFAULT, &handle) < 0) { + cxoError_raiseAndReturnNull(); + cxoBuffer_clear(&buffer); + return -1; + } + cxoBuffer_clear(&buffer); + *doc = cxoSodaDoc_new(db, handle); + if (!*doc) + return -1; + } else { + PyErr_SetString(PyExc_TypeError, + "value must be a SODA document or dictionary"); + return -1; + } + + return 0; +} + diff --git a/test/SodaCollection.py b/test/SodaCollection.py new file mode 100644 index 0000000..33b6c7e --- /dev/null +++ b/test/SodaCollection.py @@ -0,0 +1,194 @@ +#------------------------------------------------------------------------------ +# Copyright 2018, Oracle and/or its affiliates. All rights reserved. +#------------------------------------------------------------------------------ + +"""Module for testing Simple Oracle Document Access (SODA) Collections""" + +class TestSodaDocuments(BaseTestCase): + + def testInvalidJson(self): + "test inserting invalid JSON value into SODA collection" + invalidJson = "{testKey:testValue}" + sodaDatabase = self.connection.getSodaDatabase() + coll = sodaDatabase.createCollection("cxoInvalidJSON") + doc = sodaDatabase.createDocument(invalidJson) + self.assertRaises(cx_Oracle.IntegrityError, coll.insertOne, doc) + coll.drop() + + def testInsertDocuments(self): + "test inserting documents into a SODA collection" + sodaDatabase = self.connection.getSodaDatabase() + coll = sodaDatabase.createCollection("cxoInsertDocs") + coll.find().remove() + valuesToInsert = [ + { "name" : "George", "age" : 47 }, + { "name" : "Susan", "age" : 39 }, + { "name" : "John", "age" : 50 }, + { "name" : "Jill", "age" : 54 } + ] + insertedKeys = [] + for value in valuesToInsert: + doc = coll.insertOneAndGet(value) + insertedKeys.append(doc.key) + self.connection.commit() + self.assertEqual(coll.find().count(), len(valuesToInsert)) + for key, value in zip(insertedKeys, valuesToInsert): + doc = coll.find().key(key).getOne() + self.assertEqual(doc.getContent(), value) + coll.drop() + + def testSkipDocuments(self): + "test skip documents from SODA collection" + sodaDatabase = self.connection.getSodaDatabase() + coll = sodaDatabase.createCollection("cxoSkipDocs") + coll.find().remove() + valuesToInsert = [ + { "name" : "Matthew", "age" : 28 }, + { "name" : "Martha", "age" : 43 }, + { "name" : "Mark", "age" : 37 }, + { "name" : "Anna", "age" : 62 } + ] + for value in valuesToInsert: + coll.insertOne(value) + self.connection.commit() + self.assertEqual(coll.find().skip(1).getOne().getContent(), + valuesToInsert[1]) + self.assertEqual(coll.find().skip(3).getOne().getContent(), + valuesToInsert[3]) + self.assertEqual(coll.find().skip(4).getOne(), None) + self.assertEqual(coll.find().skip(125).getOne(), None) + + def testReplaceDocument(self): + "test replace documents in SODA collection" + sodaDatabase = self.connection.getSodaDatabase() + coll = sodaDatabase.createCollection("cxoReplaceDoc") + coll.find().remove() + content = {'name': 'John', 'address': {'city': 'Sydney'}} + doc = coll.insertOneAndGet(content) + newContent = {'name': 'John', 'address': {'city':'Melbourne'}} + coll.find().key(doc.key).replaceOne(newContent) + self.connection.commit() + self.assertEqual(coll.find().key(doc.key).getOne().getContent(), + newContent) + coll.drop() + + def testSearchDocumentsWithContent(self): + "test search documents with content using $like and $regex" + sodaDatabase = self.connection.getSodaDatabase() + coll = sodaDatabase.createCollection("cxoSearchDocContent") + coll.find().remove() + data = [ + {'name': 'John', 'address': {'city': 'Bangalore'}}, + {'name': 'Johnson', 'address': {'city': 'Banaras'}}, + {'name': 'Joseph', 'address': {'city': 'Bangalore'}}, + {'name': 'Jibin', 'address': {'city': 'Secunderabad'}}, + {'name': 'Andrew', 'address': {'city': 'Hyderabad'}}, + {'name': 'Matthew', 'address': {'city': 'Mumbai'}} + ] + for value in data: + coll.insertOne(value) + self.connection.commit() + filterSpecs = [ + ({'name': {'$like': 'And%'}}, 1), + ({'name': {'$like': 'J%n'}}, 3), + ({'name': {'$like': '%hn%'}}, 2), + ({'address.city': {'$like': 'Ban%'}}, 3), + ({'address.city': {'$like': '%bad'}}, 2), + ({'address.city': {'$like': 'Hyderabad'}}, 1), + ({'address.city': {'$like': 'China%'}}, 0), + ({'name': {'$regex': 'Jo.*'}}, 3), + ({'name': {'$regex': '.*[ho]n'}}, 2), + ({'name': {'$regex': 'J.*h'}}, 1), + ({'address.city': {'$regex': 'Ba.*'}}, 3), + ({'address.city': {'$regex': '.*bad'}}, 2), + ({'address.city': {'$regex': 'Hyderabad'}}, 1), + ({'name': {'$regex': 'Js.*n'}}, 0) + ] + for filterSpec, expectedCount in filterSpecs: + self.assertEqual(coll.find().filter(filterSpec).count(), + expectedCount, filterSpec) + coll.drop() + + def testDocumentRemove(self): + "test removing documents" + sodaDatabase = self.connection.getSodaDatabase() + coll = sodaDatabase.createCollection("cxoRemoveDocs") + coll.find().remove() + data = [ + {'name': 'John', 'address': {'city': 'Bangalore'}}, + {'name': 'Johnson', 'address': {'city': 'Banaras'}}, + {'name': 'Joseph', 'address': {'city': 'Mangalore'}}, + {'name': 'Jibin', 'address': {'city': 'Secunderabad'}}, + {'name': 'Andrew', 'address': {'city': 'Hyderabad'}}, + {'name': 'Matthew', 'address': {'city': 'Mumbai'}} + ] + docs = [coll.insertOneAndGet(v) for v in data] + coll.find().key(docs[3].key).remove() + self.assertEqual(coll.find().count(), len(data) - 1) + searchResults = coll.find().filter({'name': {'$like': 'Jibin'}}) + self.assertEqual(searchResults.count(), 0) + coll.find().filter({'name': {'$like': 'John%'}}).remove() + self.assertEqual(coll.find().count(), len(data) - 3) + coll.find().filter({'name': {'$regex': 'J.*'}}).remove() + self.assertEqual(coll.find().count(), len(data) - 4) + self.connection.commit() + coll.drop() + + def testCreateAndDropIndex(self): + "test create and drop Index" + indexName = "cxoTestIndexes_ix_1" + indexSpec = { + 'name': indexName, + 'fields': [ + { + 'path': 'address.city', + 'datatype': 'string', + 'order': 'asc' + } + ] + } + sodaDatabase = self.connection.getSodaDatabase() + coll = sodaDatabase.createCollection("cxoTestIndexes") + coll.find().remove() + self.connection.commit() + coll.dropIndex(indexName) + coll.createIndex(indexSpec) + self.assertRaises(cx_Oracle.DatabaseError, coll.createIndex, indexSpec) + self.assertEqual(coll.dropIndex(indexName), True) + self.assertEqual(coll.dropIndex(indexName), False) + coll.drop() + + def testGetDocuments(self): + "test getting documents from Collection" + self.connection.autocommit = True + sodaDatabase = self.connection.getSodaDatabase() + coll = sodaDatabase.createCollection("cxoTestGetDocs") + coll.find().remove() + data = [ + {'name': 'John', 'address': {'city': 'Bangalore'}}, + {'name': 'Johnson', 'address': {'city': 'Banaras'}}, + {'name': 'Joseph', 'address': {'city': 'Mangalore'}}, + {'name': 'Jibin', 'address': {'city': 'Secunderabad'}}, + {'name': 'Andrew', 'address': {'city': 'Hyderabad'}} + ] + insertedKeys = list(sorted(coll.insertOneAndGet(v).key for v in data)) + fetchedKeys = list(sorted(d.key for d in coll.find().getDocuments())) + self.assertEqual(fetchedKeys, insertedKeys) + coll.drop() + + def testCursor(self): + "test fetching documents from a cursor" + self.connection.autocommit = True + sodaDatabase = self.connection.getSodaDatabase() + coll = sodaDatabase.createCollection("cxoFindViaCursor") + coll.find().remove() + data = [ + {'name': 'John', 'address': {'city': 'Bangalore'}}, + {'name': 'Johnson', 'address': {'city': 'Banaras'}}, + {'name': 'Joseph', 'address': {'city': 'Mangalore'}}, + ] + insertedKeys = list(sorted(coll.insertOneAndGet(v).key for v in data)) + fetchedKeys = list(sorted(d.key for d in coll.find().getCursor())) + self.assertEqual(fetchedKeys, insertedKeys) + coll.drop() + diff --git a/test/SodaDatabase.py b/test/SodaDatabase.py new file mode 100644 index 0000000..a15d1d5 --- /dev/null +++ b/test/SodaDatabase.py @@ -0,0 +1,101 @@ +#------------------------------------------------------------------------------ +# Copyright 2018, Oracle and/or its affiliates. All rights reserved. +#------------------------------------------------------------------------------ + +"""Module for testing Simple Oracle Document Access (SODA) Database""" + +import json + +class TestSodaCollection(BaseTestCase): + + def __dropExistingCollections(self, sodaDatabase): + for name in sodaDatabase.getCollectionNames(): + sodaDatabase.openCollection(name).drop() + + def __verifyDocument(self, doc, rawContent, strContent=None, content=None, + key=None, mediaType='application/json'): + self.assertEqual(doc.getContentAsBytes(), rawContent) + if strContent is not None: + self.assertEqual(doc.getContentAsString(), strContent) + if content is not None: + self.assertEqual(doc.getContent(), content) + self.assertEqual(doc.key, key) + self.assertEqual(doc.mediaType, mediaType) + + def testCreateDocumentWithJson(self): + "test creating documents with JSON data" + sodaDatabase = self.connection.getSodaDatabase() + val = {"testKey1" : "testValue1", "testKey2" : "testValue2" } + strVal = json.dumps(val) + bytesVal = strVal.encode("UTF-8") + key = "MyKey" + mediaType = "text/plain" + doc = sodaDatabase.createDocument(val) + self.__verifyDocument(doc, bytesVal, strVal, val) + doc = sodaDatabase.createDocument(strVal, key) + self.__verifyDocument(doc, bytesVal, strVal, val, key) + doc = sodaDatabase.createDocument(bytesVal, key, mediaType) + self.__verifyDocument(doc, bytesVal, strVal, val, key, mediaType) + + def testCreateDocumentWithRaw(self): + "test creating documents with raw data" + sodaDatabase = self.connection.getSodaDatabase() + val = b"" + key = "MyRawKey" + mediaType = "text/html" + doc = sodaDatabase.createDocument(val) + self.__verifyDocument(doc, val) + doc = sodaDatabase.createDocument(val, key) + self.__verifyDocument(doc, val, key=key) + doc = sodaDatabase.createDocument(val, key, mediaType) + self.__verifyDocument(doc, val, key=key, mediaType=mediaType) + + def testGetCollectionNames(self): + "test getting collection names from the database" + sodaDatabase = self.connection.getSodaDatabase() + self.__dropExistingCollections(sodaDatabase) + self.assertEqual(sodaDatabase.getCollectionNames(), []) + names = ["zCol", "dCol", "sCol", "aCol", "gCol"] + sortedNames = list(sorted(names)) + for name in names: + sodaDatabase.createCollection(name) + self.assertEqual(sodaDatabase.getCollectionNames(), sortedNames) + self.assertEqual(sodaDatabase.getCollectionNames(limit=2), + sortedNames[:2]) + self.assertEqual(sodaDatabase.getCollectionNames("a"), sortedNames) + self.assertEqual(sodaDatabase.getCollectionNames("C"), sortedNames) + self.assertEqual(sodaDatabase.getCollectionNames("b", limit=3), + sortedNames[1:4]) + self.assertEqual(sodaDatabase.getCollectionNames("z"), + sortedNames[-1:]) + + def testOpenCollection(self): + "test opening a collection" + sodaDatabase = self.connection.getSodaDatabase() + self.__dropExistingCollections(sodaDatabase) + coll = sodaDatabase.openCollection("CollectionThatDoesNotExist") + self.assertEqual(coll, None) + createdColl = sodaDatabase.createCollection("cxoTestOpenCollection") + coll = sodaDatabase.openCollection(createdColl.name) + self.assertEqual(coll.name, createdColl.name) + coll.drop() + + def testRepr(self): + "test SodaDatabase representation" + con1 = self.connection + con2 = self.getConnection() + sodaDatabase1 = con1.getSodaDatabase() + sodaDatabase2 = con1.getSodaDatabase() + sodaDatabase3 = con2.getSodaDatabase() + self.assertEqual(str(sodaDatabase1), str(sodaDatabase2)) + self.assertEqual(str(sodaDatabase2), str(sodaDatabase3)) + + def testNegative(self): + "test negative cases for SODA database methods" + sodaDatabase = self.connection.getSodaDatabase() + self.assertRaises(TypeError, sodaDatabase.createCollection) + self.assertRaises(TypeError, sodaDatabase.createCollection, 1) + self.assertRaises(cx_Oracle.DatabaseError, + sodaDatabase.createCollection, None) + self.assertRaises(TypeError, sodaDatabase.getCollectionNames, 1) + diff --git a/test/sql/SetupTest.sql b/test/sql/SetupTest.sql index aa1e314..dd87b44 100644 --- a/test/sql/SetupTest.sql +++ b/test/sql/SetupTest.sql @@ -49,6 +49,19 @@ grant execute on dbms_aqadm to &main_user; grant execute on dbms_transform to &main_user; +begin + + for r in + ( select role + from dba_roles + where role in ('SODA_APP') + ) loop + execute immediate 'grant ' || r.role || ' to &main_user'; + end loop; + +end; +/ + -- create types create type &main_user..udt_SubObject as object ( SubNumberValue number, diff --git a/test/test.py b/test/test.py index 4fa9cc8..d16c66b 100644 --- a/test/test.py +++ b/test/test.py @@ -60,6 +60,9 @@ else: if clientVersion[:2] >= (12, 1): moduleNames.append("BooleanVar") moduleNames.append("Features12_1") + if clientVersion[:2] >= (18, 3): + moduleNames.append("SodaDatabase") + moduleNames.append("SodaCollection") class BaseTestCase(unittest.TestCase):