From 59c41535e33a11b07747d43151ca43a8b6c89ff1 Mon Sep 17 00:00:00 2001 From: Anthony Tuininga Date: Wed, 25 May 2022 14:37:50 -0600 Subject: [PATCH 01/13] Improved AQ test. --- test/test_3000_subscription.py | 83 ++++++++++++++++++++++++++-------- 1 file changed, 64 insertions(+), 19 deletions(-) diff --git a/test/test_3000_subscription.py b/test/test_3000_subscription.py index 595ebe0..daa5c52 100644 --- a/test/test_3000_subscription.py +++ b/test/test_3000_subscription.py @@ -7,39 +7,60 @@ """ import threading +import unittest import cx_Oracle as oracledb import test_env -class SubscriptionData(object): +class SubscriptionData: def __init__(self, num_messages_expected): self.condition = threading.Condition() self.num_messages_expected = num_messages_expected self.num_messages_received = 0 - self.table_operations = [] - self.row_operations = [] - self.rowids = [] - def CallbackHandler(self, message): + def _process_message(self, message): + pass + + def callback_handler(self, message): if message.type != oracledb.EVENT_DEREG: - table, = message.tables - self.table_operations.append(table.operation) - for row in table.rows: - self.row_operations.append(row.operation) - self.rowids.append(row.rowid) + self._process_message(message) self.num_messages_received += 1 if message.type == oracledb.EVENT_DEREG or \ self.num_messages_received == self.num_messages_expected: - self.condition.acquire() - self.condition.notify() - self.condition.release() + with self.condition: + self.condition.notify() + + def wait_for_messages(self): + if self.num_messages_received < self.num_messages_expected: + with self.condition: + self.condition.wait(10) + + +class AQSubscriptionData(SubscriptionData): + pass + + +class DMLSubscriptionData(SubscriptionData): + + def __init__(self, num_messages_expected): + super().__init__(num_messages_expected) + self.table_operations = [] + self.row_operations = [] + self.rowids = [] + + def _process_message(self, message): + table, = message.tables + self.table_operations.append(table.operation) + for row in table.rows: + self.row_operations.append(row.operation) + self.rowids.append(row.rowid) class TestCase(test_env.BaseTestCase): - def test_3000_subscription(self): - "3000 - test Subscription for insert, update, delete and truncate" + def test_3000_dml_subscription(self): + "3000 - test subscription for insert, update, delete and truncate" # skip if running on the Oracle Cloud, which does not support # subscriptions currently @@ -67,9 +88,9 @@ def test_3000_subscription(self): rowids = [] # set up subscription - data = SubscriptionData(5) + data = DMLSubscriptionData(5) connection = test_env.get_connection(threaded=True, events=True) - sub = connection.subscribe(callback=data.CallbackHandler, + sub = connection.subscribe(callback=data.callback_handler, timeout=10, qos=oracledb.SUBSCR_QOS_ROWIDS) sub.registerquery("select * from TestTempTable") connection.autocommit = True @@ -105,8 +126,7 @@ def test_3000_subscription(self): cursor.execute("truncate table TestTempTable") # wait for all messages to be sent - data.condition.acquire() - data.condition.wait(10) + data.wait_for_messages() # verify the correct messages were sent self.assertEqual(data.table_operations, table_operations) @@ -134,5 +154,30 @@ def test_3001_deprecations(self): self.assertRaises(oracledb.ProgrammingError, connection.subscribe, client_initiated=True, clientInitiated=True) + @unittest.skip("multiple subscriptions cannot be created simultaneously") + def test_3002_aq_subscription(self): + "3002 - test subscription for AQ" + + # create queue and clear it of all messages + queue = self.connection.queue("TEST_RAW_QUEUE") + queue.deqoptions.wait = oracledb.DEQ_NO_WAIT + while queue.deqone(): + pass + self.connection.commit() + + # set up subscription + data = AQSubscriptionData(1) + connection = test_env.get_connection(events=True) + sub = connection.subscribe(namespace=oracledb.SUBSCR_NAMESPACE_AQ, + name=queue.name, timeout=10, + callback=data.callback_handler) + + # enqueue a message + queue.enqone(self.connection.msgproperties(payload="Some data")) + self.connection.commit() + + # wait for all messages to be sent + data.wait_for_messages() + if __name__ == "__main__": test_env.run_test_cases() From 3db3e3772efee2dcae78bb90c2b79d7c505f5722 Mon Sep 17 00:00:00 2001 From: Anthony Tuininga Date: Wed, 25 May 2022 14:40:44 -0600 Subject: [PATCH 02/13] Fix stale OCA link. --- .github/pull_request_template.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 88ff4aa..5213271 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,8 +1,7 @@ Thanks for contributing! Before submitting PRs for cx_Oracle you must have your signed *Oracle -Contributor Agreement* accepted. See -https://www.oracle.com/technetwork/community/oca-486395.html +Contributor Agreement* accepted. See https://oca.opensource.oracle.com If the problem solved is small, you may find it easier to open an Issue describing the problem and its cause so we can create the fix. From 1ad43aa912bd4c95a312d1bed12e749d39eb1ee6 Mon Sep 17 00:00:00 2001 From: Anthony Tuininga Date: Wed, 25 May 2022 14:41:35 -0600 Subject: [PATCH 03/13] Doc improvements. --- doc/src/api_manual/cursor.rst | 2 +- doc/src/api_manual/module.rst | 2 +- doc/src/api_manual/session_pool.rst | 2 +- doc/src/user_guide/connection_handling.rst | 12 ++++++------ doc/src/user_guide/cqn.rst | 19 ++++++++++--------- doc/src/user_guide/initialization.rst | 2 +- doc/src/user_guide/installation.rst | 9 +++++---- 7 files changed, 25 insertions(+), 23 deletions(-) diff --git a/doc/src/api_manual/cursor.rst b/doc/src/api_manual/cursor.rst index b89b2f1..0b00c91 100644 --- a/doc/src/api_manual/cursor.rst +++ b/doc/src/api_manual/cursor.rst @@ -290,7 +290,7 @@ Cursor Object See :ref:`fetching` for an example. -.. method:: Cursor.fetchmany(num_rows=cursor.arraysize) +.. method:: Cursor.fetchmany(numRows=cursor.arraysize) Fetch the next set of rows of a query result, returning a list of tuples. An empty list is returned if no more rows are available. Note that the diff --git a/doc/src/api_manual/module.rst b/doc/src/api_manual/module.rst index dac1a2f..7eff2ad 100644 --- a/doc/src/api_manual/module.rst +++ b/doc/src/api_manual/module.rst @@ -40,7 +40,7 @@ Module Interface mode=cx_Oracle.DEFAULT_AUTH, handle=0, pool=None, threaded=False, \ events=False, cclass=None, purity=cx_Oracle.ATTR_PURITY_DEFAULT, \ newpassword=None, encoding=None, nencoding=None, edition=None, \ - appcontext=[], tag=None, matchanytag=None, shardingkey=[], \ + appcontext=[], tag=None, matchanytag=False, shardingkey=[], \ supershardingkey=[], stmtcachesize=20) Connection(user=None, password=None, dsn=None, \ mode=cx_Oracle.DEFAULT_AUTH, handle=0, pool=None, threaded=False, \ diff --git a/doc/src/api_manual/session_pool.rst b/doc/src/api_manual/session_pool.rst index 5ef8222..b73ce41 100644 --- a/doc/src/api_manual/session_pool.rst +++ b/doc/src/api_manual/session_pool.rst @@ -160,7 +160,7 @@ SessionPool Object unusable, it is discarded and a different connection is selected to be returned by :meth:`SessionPool.acquire()`. Setting ``ping_interval`` to a negative value disables pinging. Setting it to 0 forces a ping for every - ``aquire()`` and is not recommended. + ``acquire()`` and is not recommended. Prior to cx_Oracle 8.2, the ping interval was fixed at 60 seconds. diff --git a/doc/src/user_guide/connection_handling.rst b/doc/src/user_guide/connection_handling.rst index e80e4db..0b24bb8 100644 --- a/doc/src/user_guide/connection_handling.rst +++ b/doc/src/user_guide/connection_handling.rst @@ -336,7 +336,7 @@ connection pool: pool.close() Other :meth:`cx_Oracle.SessionPool()` options can be used at pool creation. -For example the ``getmode`` value can be set so that any ``aquire()`` call will +For example the ``getmode`` value can be set so that any ``acquire()`` call will wait for a connection to become available if all are currently in use, for example: @@ -349,8 +349,8 @@ example: getmode=cx_Oracle.SPOOL_ATTRVAL_WAIT, encoding="UTF-8") -See `ConnectionPool.py -`__ +See `connection_pool.py +`__ for an example. Before :meth:`SessionPool.acquire()` returns, cx_Oracle does a lightweight check @@ -1348,10 +1348,10 @@ of the function :meth:`cx_Oracle.connect()` constructor: dsn="dbhost.example.com/orclpdb1", newpassword=newpwd, encoding="UTF-8") -.. _autononmousdb: +.. _autonomousdb: -Connecting to Oracle Cloud Autononmous Databases -================================================ +Connecting to Oracle Cloud Autonomous Databases +=============================================== To enable connection to Oracle Autonomous Database in Oracle Cloud, a wallet needs be downloaded from the cloud, and cx_Oracle needs to be configured to use diff --git a/doc/src/user_guide/cqn.rst b/doc/src/user_guide/cqn.rst index d4d2293..1697d40 100644 --- a/doc/src/user_guide/cqn.rst +++ b/doc/src/user_guide/cqn.rst @@ -19,14 +19,14 @@ what types of SQL should trigger a notification, whether notifications should survive database loss, and control over unsubscription. You can also choose whether notification messages will include ROWIDs of affected rows. -By default, object-level (previously known as Database Change Notification) -occurs and the Python notification method is invoked whenever a database -transaction is committed that changes an object that a registered query -references, regardless of whether the actual query result changed. However if -the :meth:`subscription ` option ``qos`` is -:data:`cx_Oracle.SUBSCR_QOS_QUERY` then query-level notification occurs. In -this mode, the database notifies the application whenever a transaction changing -the result of the registered query is committed. +By default, object-level notification (previously known as Database Change +Notification) occurs. With this mode a Python notification method is invoked +whenever a database transaction is committed that changes an object referenced +by a registered query. However if the :meth:`subscription +` option ``qos`` is :data:`cx_Oracle.SUBSCR_QOS_QUERY` +then query-level notification occurs. In this mode, the database notifies the +application whenever a committed transaction changes the result of a registered +query. CQN is best used to track infrequent data changes. @@ -109,7 +109,7 @@ calling :meth:`Subscription.registerquery()`. Registering a query behaves similarly to :meth:`Cursor.execute()`, but only queries are permitted and the ``args`` parameter must be a sequence or dictionary. -An example script to receive query notifications when the 'CUSTOMER' table data +An example script to receive query notifications when the 'REGIONS' table data changes is: .. code-block:: python @@ -129,6 +129,7 @@ changes is: subscr = connection.subscribe(callback=cqn_callback, operations=cx_Oracle.OPCODE_INSERT | cx_Oracle.OPCODE_DELETE, qos=cx_Oracle.SUBSCR_QOS_QUERY | cx_Oracle.SUBSCR_QOS_ROWIDS) + subscr.registerquery("select * from regions") input("Hit enter to stop CQN demo\n") diff --git a/doc/src/user_guide/initialization.rst b/doc/src/user_guide/initialization.rst index 5abd929..c2fa3ca 100644 --- a/doc/src/user_guide/initialization.rst +++ b/doc/src/user_guide/initialization.rst @@ -241,7 +241,7 @@ located with, or separately from, the ``tnsnames.ora`` and ``sqlnet.ora`` files. It should be securely stored. The ``sqlnet.ora`` file's ``WALLET_LOCATION`` path should be set to the directory containing ``cwallet.sso``. For Oracle Autonomous Database use of wallets, see -:ref:`autononmousdb`. +:ref:`autonomousdb`. Note the :ref:`easyconnect` can set many common configuration options without needing ``tnsnames.ora`` or ``sqlnet.ora`` files. diff --git a/doc/src/user_guide/installation.rst b/doc/src/user_guide/installation.rst index 62d9f84..9f5a9f7 100644 --- a/doc/src/user_guide/installation.rst +++ b/doc/src/user_guide/installation.rst @@ -491,10 +491,11 @@ To use cx_Oracle with Oracle Instant Client zip files: a 64-bit or 32-bit architecture to match Instant Client's architecture. Each Instant Client version requires a different redistributable version: - - For Instant Client 19 install `VS 2017 `__. - - For Instant Client 18 or 12.2 install `VS 2013 `__ - - For Instant Client 12.1 install `VS 2010 `__ - - For Instant Client 11.2 install `VS 2005 64-bit `__ or `VS 2005 32-bit `__ + - For Instant Client 21 install `VS 2019 `__ or later. + - For Instant Client 19 install `VS 2017 `__. + - For Instant Client 18 or 12.2 install `VS 2013 `__ + - For Instant Client 12.1 install `VS 2010 `__ + - For Instant Client 11.2 install `VS 2005 64-bit `__ Configure Oracle Instant Client ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From fde577bf1fb15448e0865482fb57d477e7496a10 Mon Sep 17 00:00:00 2001 From: Anthony Tuininga Date: Wed, 25 May 2022 14:43:26 -0600 Subject: [PATCH 04/13] python-oracledb 1.0.0 has been released! --- README.md | 24 +++++++++++++---- README.txt | 6 +++-- doc/src/api_manual/module.rst | 7 +++++ doc/src/index.rst | 7 +++++ doc/src/user_guide/installation.rst | 42 ++++++++++++++++++++++------- doc/src/user_guide/introduction.rst | 13 ++++----- samples/README.md | 14 +++++++++- 7 files changed, 89 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 346711a..64edd71 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,18 @@ -# cx_Oracle version 8.3 +# Python cx_Oracle + +# News + +**cx_Oracle has a major new release under a new name and homepage +[python-oracledb](https://oracle.github.io/python-oracledb/).** + +**The source code has moved to +[github.com/oracle/python-oracledb](https://github.com/oracle/python-oracledb).** + +New projects should install python-oracledb instead of cx_Oracle. Critical +patches and binary packages for new Python releases may continue to be made in +the cx_Oracle namespace for a limited time, subject to demand. + +# About cx_Oracle is a Python extension module that enables access to Oracle Database. It conforms to the [Python database API 2.0 @@ -7,7 +21,7 @@ of exclusions. See the [homepage](https://oracle.github.io/python-cx_Oracle/index.html) for a feature list. -cx_Oracle 8.3 has been tested with Python versions 3.6 through 3.10. You can +cx_Oracle 8.3 was tested with Python versions 3.6 through 3.10. You can use cx_Oracle with Oracle 11.2, 12c, 18c, 19c and 21c client libraries. Oracle's standard client-server version interoperability allows connection to both older and newer databases. For example Oracle 19c client libraries can @@ -45,10 +59,10 @@ See [CONTRIBUTING](https://github.com/oracle/python-cx_Oracle/blob/main/CONTRIBU cx_Oracle is licensed under a BSD license which you can find [here][3]. -[1]: https://www.python.org/dev/peps/pep-0249 -[2]: http://cx-oracle.readthedocs.io +[1]: https://peps.python.org/pep-0249/ +[2]: https://cx-oracle.readthedocs.io [3]: https://github.com/oracle/python-cx_Oracle/blob/main/LICENSE.txt -[5]: http://lists.sourceforge.net/lists/listinfo/cx-oracle-users +[5]: https://sourceforge.net/projects/cx-oracle/lists/cx-oracle-users [6]: https://github.com/oracle/python-cx_Oracle/tree/main/samples/tutorial [7]: http://cx-oracletools.sourceforge.net [8]: http://cx-pyoraclelib.sourceforge.net diff --git a/README.txt b/README.txt index 46124e2..a709cfc 100644 --- a/README.txt +++ b/README.txt @@ -1,5 +1,7 @@ -Please see the cx_Oracle home page for links to documentation, source, build -and installation instructions: +An enhanced cx_Oracle release is now under the python-oracledb namespace. See +https://oracle.github.io/python-oracledb/index.html for how to install and use +this updated driver. +For information about cx_Oracle itself, see https://oracle.github.io/python-cx_Oracle/index.html diff --git a/doc/src/api_manual/module.rst b/doc/src/api_manual/module.rst index 7eff2ad..ee215fc 100644 --- a/doc/src/api_manual/module.rst +++ b/doc/src/api_manual/module.rst @@ -6,6 +6,13 @@ Module Interface **************** +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + .. data:: __future__ Special object which contains attributes which control the behavior of diff --git a/doc/src/index.rst b/doc/src/index.rst index 1dfd77a..019c234 100644 --- a/doc/src/index.rst +++ b/doc/src/index.rst @@ -11,6 +11,13 @@ releases. (the BSD license). A detailed description of cx_Oracle changes can be found in the :ref:`release notes `. +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + Contents: User Guide diff --git a/doc/src/user_guide/installation.rst b/doc/src/user_guide/installation.rst index 9f5a9f7..69c871a 100644 --- a/doc/src/user_guide/installation.rst +++ b/doc/src/user_guide/installation.rst @@ -4,12 +4,19 @@ cx_Oracle 8 Installation ************************ +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + Overview ======== -To use cx_Oracle 8 with Python and Oracle Database you need: +To use cx_Oracle 8.3 with Python and Oracle Database you need: -- Python 3.5 and higher. Older versions of cx_Oracle may work with older +- Python 3.6 and higher. Older versions of cx_Oracle may work with older versions of Python. - Oracle Client libraries. These can be from the free `Oracle Instant Client @@ -35,18 +42,12 @@ product: it is how the Oracle Client and Oracle Database communicate. Quick Start cx_Oracle Installation ================================== -The `Quick Start: Developing Python Applications for Oracle Database -`__ -and `Quick Start: Developing Python Applications for Oracle Autonomous Database -`__ -instructions have steps for Windows, Linux, and macOS. - -Alternatively you can: +You can: - Install `Python `__ 3, if not already available. On macOS you must always install your own Python. - Python 3.5 and higher are supported by cx_Oracle 8. If you use Python 2, + Python 3.6 and higher are supported by cx_Oracle 8.3. If you use Python 2, then the older cx_Oracle 7.3 will install. - Install cx_Oracle from `PyPI @@ -191,6 +192,13 @@ Installing cx_Oracle on Linux This section discusses the generic installation methods on Linux. To use Python and cx_Oracle RPM packages from yum on Oracle Linux, see :ref:`oraclelinux`. +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + Install cx_Oracle ----------------- @@ -424,6 +432,13 @@ Developers `__. Installing cx_Oracle on Windows =============================== +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + Install cx_Oracle ----------------- @@ -572,6 +587,13 @@ Python architecture. Installing cx_Oracle on macOS (Intel x86) ========================================= +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + Install Python -------------- diff --git a/doc/src/user_guide/introduction.rst b/doc/src/user_guide/introduction.rst index 9e9ddb0..c917a16 100644 --- a/doc/src/user_guide/introduction.rst +++ b/doc/src/user_guide/introduction.rst @@ -9,6 +9,13 @@ Database. It conforms to the `Python Database API v2.0 Specification `__ with a considerable number of additions and a couple of exclusions. +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + Architecture ------------ @@ -131,12 +138,6 @@ The output is:: Examples and Tutorials ---------------------- -The `Quick Start: Developing Python Applications for Oracle Database -`__ -and `Quick Start: Developing Python Applications for Oracle Autonomous Database -`__ -instructions have steps for Windows, Linux, and macOS. - Runnable examples are in the `GitHub samples directory `__. A `Python cx_Oracle tutorial diff --git a/samples/README.md b/samples/README.md index 81c2286..983b4ce 100644 --- a/samples/README.md +++ b/samples/README.md @@ -1,4 +1,16 @@ -# cx_Oracle Examples +# Samples + +## News + +**cx_Oracle has a major new release under a new name and homepage +[python-oracledb](https://oracle.github.io/python-oracledb/).** + +**New projects should install python-oracledb instead of cx_Oracle.** + +**The new source code and samples can be found at +[github.com/oracle/python-oracledb](https://github.com/oracle/python-oracledb).** + +## cx_Oracle Examples This directory contains samples for [cx_Oracle][6]. Documentation is [here][7]. A separate tutorial is [here][8]. From 5cfbb7d9e49f23a67332f37983dad249718db3f4 Mon Sep 17 00:00:00 2001 From: Anthony Tuininga Date: Wed, 8 Jun 2022 13:12:35 -0600 Subject: [PATCH 05/13] Remove semicolons in Python code examples (resolves #629). --- doc/src/user_guide/initialization.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/src/user_guide/initialization.rst b/doc/src/user_guide/initialization.rst index c2fa3ca..f2e3c08 100644 --- a/doc/src/user_guide/initialization.rst +++ b/doc/src/user_guide/initialization.rst @@ -168,8 +168,8 @@ example, if the Oracle Instant Client Libraries are in cx_Oracle.init_oracle_client(lib_dir=lib_dir) except Exception as err: print("Whoops!") - print(err); - sys.exit(1); + print(err) + sys.exit(1) Note the use of a 'raw' string ``r"..."`` on Windows so that backslashes are treated as directory separators. @@ -220,8 +220,8 @@ you can call :meth:`cx_Oracle.init_oracle_client()`: cx_Oracle.init_oracle_client(config_dir="/etc/my-oracle-config") except Exception as err: print("Whoops!") - print(err); - sys.exit(1); + print(err) + sys.exit(1) This is equivalent to setting the environment variable `TNS_ADMIN `__ @@ -393,8 +393,8 @@ parameters is: error_url="https://example.com/MyInstallInstructions.html") except Exception as err: print("Whoops!") - print(err); - sys.exit(1); + print(err) + sys.exit(1) The convention for ``driver_name`` is to separate the product name from the product version by a colon and single blank characters. The value will be shown From a05b9a5233842ef3c4a297ccca8a34fd57d51af5 Mon Sep 17 00:00:00 2001 From: Anthony Tuininga Date: Sat, 3 Jun 2023 09:37:29 -0600 Subject: [PATCH 06/13] Add more doc redirects. --- doc/src/api_manual/aq.rst | 7 +++++++ doc/src/api_manual/connection.rst | 7 +++++++ doc/src/api_manual/cursor.rst | 7 +++++++ doc/src/api_manual/deprecations.rst | 7 +++++++ doc/src/api_manual/lob.rst | 7 +++++++ doc/src/api_manual/object_type.rst | 7 +++++++ doc/src/api_manual/session_pool.rst | 7 +++++++ doc/src/api_manual/soda.rst | 7 +++++++ doc/src/api_manual/subscription.rst | 7 +++++++ doc/src/api_manual/variable.rst | 7 +++++++ doc/src/index.rst | 14 +++++++------- doc/src/release_notes.rst | 7 +++++++ doc/src/user_guide/aq.rst | 7 +++++++ doc/src/user_guide/batch_statement.rst | 7 +++++++ doc/src/user_guide/bind.rst | 7 +++++++ doc/src/user_guide/connection_handling.rst | 7 +++++++ doc/src/user_guide/cqn.rst | 7 +++++++ doc/src/user_guide/exception_handling.rst | 7 +++++++ doc/src/user_guide/globalization.rst | 7 +++++++ doc/src/user_guide/ha.rst | 7 +++++++ doc/src/user_guide/initialization.rst | 7 +++++++ doc/src/user_guide/json_data_type.rst | 7 +++++++ doc/src/user_guide/lob_data.rst | 7 +++++++ doc/src/user_guide/plsql_execution.rst | 7 +++++++ doc/src/user_guide/soda.rst | 7 +++++++ doc/src/user_guide/sql_execution.rst | 7 +++++++ doc/src/user_guide/startup.rst | 7 +++++++ doc/src/user_guide/tracing_sql.rst | 7 +++++++ doc/src/user_guide/tuning.rst | 7 +++++++ doc/src/user_guide/txn_management.rst | 7 +++++++ doc/src/user_guide/xml_data_type.rst | 7 +++++++ 31 files changed, 217 insertions(+), 7 deletions(-) diff --git a/doc/src/api_manual/aq.rst b/doc/src/api_manual/aq.rst index ad9c9e8..584eef9 100644 --- a/doc/src/api_manual/aq.rst +++ b/doc/src/api_manual/aq.rst @@ -4,6 +4,13 @@ Advanced Queuing (AQ) ********************* +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + See :ref:`aqusermanual` for more information about using AQ in cx_Oracle. .. note:: diff --git a/doc/src/api_manual/connection.rst b/doc/src/api_manual/connection.rst index 4694822..77b84e6 100644 --- a/doc/src/api_manual/connection.rst +++ b/doc/src/api_manual/connection.rst @@ -4,6 +4,13 @@ Connection Object ***************** +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + .. note:: Any outstanding changes will be rolled back when the connection object diff --git a/doc/src/api_manual/cursor.rst b/doc/src/api_manual/cursor.rst index 0b00c91..ee0b9c6 100644 --- a/doc/src/api_manual/cursor.rst +++ b/doc/src/api_manual/cursor.rst @@ -5,6 +5,13 @@ Cursor Object ************* +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + .. method:: Cursor.__enter__() The entry point for the cursor as a context manager. It returns itself. diff --git a/doc/src/api_manual/deprecations.rst b/doc/src/api_manual/deprecations.rst index e2b7a2f..1cb0b39 100644 --- a/doc/src/api_manual/deprecations.rst +++ b/doc/src/api_manual/deprecations.rst @@ -4,6 +4,13 @@ Deprecations ************ +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + The following tables contains all of the deprecations in the cx_Oracle API, when they were first deprecated and a comment on what should be used instead, if applicable. The most recent deprecations are listed first. diff --git a/doc/src/api_manual/lob.rst b/doc/src/api_manual/lob.rst index 6ae61f8..245dabc 100644 --- a/doc/src/api_manual/lob.rst +++ b/doc/src/api_manual/lob.rst @@ -4,6 +4,13 @@ LOB Objects *********** +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + See :ref:`lobdata` for more information about using LOBs. .. note:: diff --git a/doc/src/api_manual/object_type.rst b/doc/src/api_manual/object_type.rst index 81a6d1f..5a546d6 100644 --- a/doc/src/api_manual/object_type.rst +++ b/doc/src/api_manual/object_type.rst @@ -4,6 +4,13 @@ Object Type Objects ******************* +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + .. note:: This object is an extension to the DB API. It is returned by the diff --git a/doc/src/api_manual/session_pool.rst b/doc/src/api_manual/session_pool.rst index b73ce41..aa667e8 100644 --- a/doc/src/api_manual/session_pool.rst +++ b/doc/src/api_manual/session_pool.rst @@ -4,6 +4,13 @@ SessionPool Object ****************** +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + .. note:: This object is an extension to the DB API. diff --git a/doc/src/api_manual/soda.rst b/doc/src/api_manual/soda.rst index 1e4baf7..c5a82e0 100644 --- a/doc/src/api_manual/soda.rst +++ b/doc/src/api_manual/soda.rst @@ -4,6 +4,13 @@ SODA **** +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + `Oracle Database Simple Oracle Document Access (SODA) `__ allows documents to be inserted, queried, and retrieved from Oracle Database diff --git a/doc/src/api_manual/subscription.rst b/doc/src/api_manual/subscription.rst index 264d795..e80ebd9 100644 --- a/doc/src/api_manual/subscription.rst +++ b/doc/src/api_manual/subscription.rst @@ -4,6 +4,13 @@ Subscription Object ******************* +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + .. note:: This object is an extension the DB API. diff --git a/doc/src/api_manual/variable.rst b/doc/src/api_manual/variable.rst index 4d7e599..21ca278 100644 --- a/doc/src/api_manual/variable.rst +++ b/doc/src/api_manual/variable.rst @@ -4,6 +4,13 @@ Variable Objects **************** +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + .. note:: The DB API definition does not define this object. diff --git a/doc/src/index.rst b/doc/src/index.rst index 019c234..00b3739 100644 --- a/doc/src/index.rst +++ b/doc/src/index.rst @@ -1,6 +1,13 @@ Welcome to cx_Oracle's documentation! ===================================== +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + **cx_Oracle** is a module that enables access to Oracle Database and conforms to the Python database API specification. This module is currently tested against Oracle Client 21c, 19c, 18c, 12c, and 11.2, and Python 3.6, 3.7, 3.8, @@ -11,13 +18,6 @@ releases. (the BSD license). A detailed description of cx_Oracle changes can be found in the :ref:`release notes `. -.. note:: - - **cx_Oracle has a major new release under a new name and homepage** - `python-oracledb `__. - - **New projects should install python-oracledb instead of cx_Oracle.** - Contents: User Guide diff --git a/doc/src/release_notes.rst b/doc/src/release_notes.rst index 25e335a..15c1e5c 100644 --- a/doc/src/release_notes.rst +++ b/doc/src/release_notes.rst @@ -5,6 +5,13 @@ cx_Oracle Release Notes ======================= +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + For any deprecations, see :ref:`Deprecations `. Version 8.3 (November 2021) diff --git a/doc/src/user_guide/aq.rst b/doc/src/user_guide/aq.rst index 2214e78..bbcc127 100644 --- a/doc/src/user_guide/aq.rst +++ b/doc/src/user_guide/aq.rst @@ -4,6 +4,13 @@ Oracle Advanced Queuing (AQ) **************************** +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + `Oracle Advanced Queuing `__ is a highly configurable and scalable messaging feature of Oracle Database. It has diff --git a/doc/src/user_guide/batch_statement.rst b/doc/src/user_guide/batch_statement.rst index 716c93f..94a3b6f 100644 --- a/doc/src/user_guide/batch_statement.rst +++ b/doc/src/user_guide/batch_statement.rst @@ -4,6 +4,13 @@ Batch Statement Execution and Bulk Loading ****************************************** +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + Inserting or updating multiple rows can be performed efficiently with :meth:`Cursor.executemany()`, making it easy to work with large data sets with cx_Oracle. This method can significantly outperform repeated calls to diff --git a/doc/src/user_guide/bind.rst b/doc/src/user_guide/bind.rst index 69d86ce..edb9ddc 100644 --- a/doc/src/user_guide/bind.rst +++ b/doc/src/user_guide/bind.rst @@ -4,6 +4,13 @@ Using Bind Variables ******************** +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + SQL and PL/SQL statements that pass data to and from Oracle Database should use placeholders in SQL and PL/SQL statements that mark where data is supplied or returned. These placeholders are referred to as bind variables or bind diff --git a/doc/src/user_guide/connection_handling.rst b/doc/src/user_guide/connection_handling.rst index 0b24bb8..690e9e8 100644 --- a/doc/src/user_guide/connection_handling.rst +++ b/doc/src/user_guide/connection_handling.rst @@ -4,6 +4,13 @@ Connecting to Oracle Database ***************************** +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + Connections between cx_Oracle and Oracle Database are used for executing :ref:`SQL `, :ref:`PL/SQL `, and :ref:`SODA `. diff --git a/doc/src/user_guide/cqn.rst b/doc/src/user_guide/cqn.rst index 1697d40..aa4ff30 100644 --- a/doc/src/user_guide/cqn.rst +++ b/doc/src/user_guide/cqn.rst @@ -4,6 +4,13 @@ Continuous Query Notification (CQN) *********************************** +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + `Continuous Query Notification (CQN) `__ allows applications to receive diff --git a/doc/src/user_guide/exception_handling.rst b/doc/src/user_guide/exception_handling.rst index e27075b..ffe910b 100644 --- a/doc/src/user_guide/exception_handling.rst +++ b/doc/src/user_guide/exception_handling.rst @@ -4,6 +4,13 @@ Exception Handling ****************** +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + All exceptions raised by cx_Oracle are inherited from :attr:`cx_Oracle.Error`. See :ref:`Exceptions ` for more details on the various exceptions defined by cx_Oracle. See the exception handling section in the diff --git a/doc/src/user_guide/globalization.rst b/doc/src/user_guide/globalization.rst index 2d6f947..dc35e5d 100644 --- a/doc/src/user_guide/globalization.rst +++ b/doc/src/user_guide/globalization.rst @@ -4,6 +4,13 @@ Character Sets and Globalization ******************************** +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + Data fetched from, and sent to, Oracle Database will be mapped between the database character set and the "Oracle client" character set of the Oracle Client libraries used by cx_Oracle. If data cannot be correctly mapped between diff --git a/doc/src/user_guide/ha.rst b/doc/src/user_guide/ha.rst index 70bc3e5..d3d7250 100644 --- a/doc/src/user_guide/ha.rst +++ b/doc/src/user_guide/ha.rst @@ -4,6 +4,13 @@ High Availability with cx_Oracle ******************************** +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + Applications can utilize many features for high availability (HA) during planned and unplanned outages in order to: diff --git a/doc/src/user_guide/initialization.rst b/doc/src/user_guide/initialization.rst index f2e3c08..d34fdfa 100644 --- a/doc/src/user_guide/initialization.rst +++ b/doc/src/user_guide/initialization.rst @@ -4,6 +4,13 @@ cx_Oracle 8 Initialization ************************** +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + The cx_Oracle module loads Oracle Client libraries which communicate over Oracle Net to an existing database. The Oracle Client libraries need to be installed separately. See :ref:`installation`. Oracle Net is not a separate diff --git a/doc/src/user_guide/json_data_type.rst b/doc/src/user_guide/json_data_type.rst index bbd0dfb..86d19ca 100644 --- a/doc/src/user_guide/json_data_type.rst +++ b/doc/src/user_guide/json_data_type.rst @@ -4,6 +4,13 @@ Working with the JSON Data Type ******************************* +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + Native support for JSON data was introduced in Oracle Database 12c. You can use JSON with relational database features, including transactions, indexing, declarative querying, and views. You can project JSON data relationally, diff --git a/doc/src/user_guide/lob_data.rst b/doc/src/user_guide/lob_data.rst index 39ae12b..6a503d5 100644 --- a/doc/src/user_guide/lob_data.rst +++ b/doc/src/user_guide/lob_data.rst @@ -4,6 +4,13 @@ Using CLOB and BLOB Data ************************ +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + Oracle Database uses :ref:`lobobj` to store large data such as text, images, videos and other multimedia formats. The maximum size of a LOB is limited to the size of the tablespace storing it. diff --git a/doc/src/user_guide/plsql_execution.rst b/doc/src/user_guide/plsql_execution.rst index 7a1306a..402ebc3 100644 --- a/doc/src/user_guide/plsql_execution.rst +++ b/doc/src/user_guide/plsql_execution.rst @@ -4,6 +4,13 @@ PL/SQL Execution **************** +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + PL/SQL stored procedures, functions and anonymous blocks can be called from cx_Oracle. diff --git a/doc/src/user_guide/soda.rst b/doc/src/user_guide/soda.rst index 490cafc..7b87096 100644 --- a/doc/src/user_guide/soda.rst +++ b/doc/src/user_guide/soda.rst @@ -4,6 +4,13 @@ Simple Oracle Document Access (SODA) ************************************ +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + Overview ======== diff --git a/doc/src/user_guide/sql_execution.rst b/doc/src/user_guide/sql_execution.rst index dca4179..67c7778 100644 --- a/doc/src/user_guide/sql_execution.rst +++ b/doc/src/user_guide/sql_execution.rst @@ -4,6 +4,13 @@ SQL Execution ************* +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + Executing SQL statements is the primary way in which a Python application communicates with Oracle Database. Statements are executed using the methods :meth:`Cursor.execute()` or :meth:`Cursor.executemany()`. Statements include diff --git a/doc/src/user_guide/startup.rst b/doc/src/user_guide/startup.rst index 527a877..0782b91 100644 --- a/doc/src/user_guide/startup.rst +++ b/doc/src/user_guide/startup.rst @@ -4,6 +4,13 @@ Starting and Stopping Oracle Database ************************************* +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + This chapter covers how to start up and shutdown Oracle Database using cx_Oracle. diff --git a/doc/src/user_guide/tracing_sql.rst b/doc/src/user_guide/tracing_sql.rst index a1e1d3e..5e73a39 100644 --- a/doc/src/user_guide/tracing_sql.rst +++ b/doc/src/user_guide/tracing_sql.rst @@ -4,6 +4,13 @@ Tracing SQL and PL/SQL Statements ********************************* +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + Subclass Connections ==================== diff --git a/doc/src/user_guide/tuning.rst b/doc/src/user_guide/tuning.rst index ba3bea8..993b321 100644 --- a/doc/src/user_guide/tuning.rst +++ b/doc/src/user_guide/tuning.rst @@ -4,6 +4,13 @@ Tuning cx_Oracle **************** +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + Some general tuning tips are: * Tune your application architecture. diff --git a/doc/src/user_guide/txn_management.rst b/doc/src/user_guide/txn_management.rst index 9e8985a..196d73a 100644 --- a/doc/src/user_guide/txn_management.rst +++ b/doc/src/user_guide/txn_management.rst @@ -4,6 +4,13 @@ Transaction Management ********************** +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + A database transaction is a grouping of SQL statements that make a logical data change to the database. diff --git a/doc/src/user_guide/xml_data_type.rst b/doc/src/user_guide/xml_data_type.rst index e0e0a06..44f91c7 100644 --- a/doc/src/user_guide/xml_data_type.rst +++ b/doc/src/user_guide/xml_data_type.rst @@ -4,6 +4,13 @@ Working with XMLTYPE ******************** +.. note:: + + **cx_Oracle has a major new release under a new name and homepage** + `python-oracledb `__. + + **New projects should install python-oracledb instead of cx_Oracle.** + Oracle XMLType columns are fetched as strings by default. This is currently limited to the maximum length of a ``VARCHAR2`` column. To return longer XML values, they must be queried as LOB values instead. From 6766bcaf27bf52c314219c21ffbf82d75b9a7602 Mon Sep 17 00:00:00 2001 From: Anthony Tuininga Date: Wed, 16 Aug 2023 18:33:24 -0600 Subject: [PATCH 07/13] Update ReadTheDocs configuration to avoid deprecation warnings with ReadTheDocs. --- .readthedocs.yaml | 15 +++++++++++---- doc/requirements.txt | 4 ++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index ef29602..8ecaf93 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,9 +1,16 @@ +# required version: 2 +build: + os: ubuntu-20.04 + tools: + python: "3.9" + +# Build documentation in the doc/src directory with Sphinx sphinx: - configuration: doc/src/conf.py + configuration: doc/src/conf.py +# declare Python requirements required to build docs python: - version: 3.8 - install: - - requirements: doc/requirements.txt + install: + - requirements: doc/requirements.txt diff --git a/doc/requirements.txt b/doc/requirements.txt index e18fa42..dcc8e5c 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,2 +1,2 @@ -sphinx==4.1.2 -sphinx_rtd_theme==0.5.1 +sphinx>=4.2.0 +sphinx-rtd-theme>=0.5.2 From 83774f95207e5ac0b01f2b4bb04056ddc54730aa Mon Sep 17 00:00:00 2001 From: Anthony Tuininga Date: Tue, 7 May 2024 20:33:12 -0600 Subject: [PATCH 08/13] Update templates and READMEs to take note of python-oracledb. --- .github/ISSUE_TEMPLATE/bug_report.md | 12 ++++- .../documentation-and-example-improvements.md | 15 +++---- .../ISSUE_TEMPLATE/enhancement-requests.md | 15 +++---- .../general-questions-and-runtime-problems.md | 9 +++- .../ISSUE_TEMPLATE/installation-questions.md | 9 +++- .github/SECURITY.md | 38 +++++++++++++--- .github/SUPPORT.md | 6 ++- .github/pull_request_template.md | 20 ++------- CONTRIBUTING.md | 44 ++----------------- README.md | 12 ++++- 10 files changed, 93 insertions(+), 87 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 63133f7..0af4308 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -13,7 +13,17 @@ Thank you for using cx_Oracle. See https://www.oracle.com/corporate/security-practices/assurance/vulnerability/reporting.html for how to report security issues -Please answer these questions so we can help you. +The cx_Oracle driver was renamed to python-oracledb in May 2022. It has a new +repository at https://github.com/oracle/python-oracledb. The installation +instructions are at: +https://python-oracledb.readthedocs.io/en/latest/user_guide/installation.html + +Update to python-oracledb, if possible, and submit your bug report to the +python-oracledb repository. + +No further releases under the cx_Oracle namespace are planned. + +Otherwise, please answer these questions so we can help you. Use Markdown syntax, see https://docs.github.com/github/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax diff --git a/.github/ISSUE_TEMPLATE/documentation-and-example-improvements.md b/.github/ISSUE_TEMPLATE/documentation-and-example-improvements.md index ad77f9c..cf10784 100644 --- a/.github/ISSUE_TEMPLATE/documentation-and-example-improvements.md +++ b/.github/ISSUE_TEMPLATE/documentation-and-example-improvements.md @@ -9,16 +9,13 @@ assignees: '' - -1. What is the link to the documentation section that needs improving? - -2. Describe the confusion - -3. Suggest changes that would help diff --git a/.github/ISSUE_TEMPLATE/enhancement-requests.md b/.github/ISSUE_TEMPLATE/enhancement-requests.md index 199ecba..98aaa04 100644 --- a/.github/ISSUE_TEMPLATE/enhancement-requests.md +++ b/.github/ISSUE_TEMPLATE/enhancement-requests.md @@ -9,16 +9,13 @@ assignees: '' - -1. Describe your new request in detail - -2. Give supporting information about tools and operating systems. Give relevant product version numbers diff --git a/.github/ISSUE_TEMPLATE/general-questions-and-runtime-problems.md b/.github/ISSUE_TEMPLATE/general-questions-and-runtime-problems.md index 78d9d1d..b73bf70 100644 --- a/.github/ISSUE_TEMPLATE/general-questions-and-runtime-problems.md +++ b/.github/ISSUE_TEMPLATE/general-questions-and-runtime-problems.md @@ -11,7 +11,14 @@ assignees: '' Thank you for using cx_Oracle. -Review the user manual: https://cx-oracle.readthedocs.io/en/latest/index.html +The cx_Oracle driver was renamed to python-oracledb in May 2022. It has a new +repository at https://github.com/oracle/python-oracledb. The installation +instructions are at: +https://python-oracledb.readthedocs.io/en/latest/user_guide/installation.html + +Update to python-oracledb, if possible. + +Otherwise, review the cx_Oracle user manual: https://cx-oracle.readthedocs.io/en/latest/index.html Please answer these questions so we can help you. diff --git a/.github/ISSUE_TEMPLATE/installation-questions.md b/.github/ISSUE_TEMPLATE/installation-questions.md index 0a7b2a8..be7b94b 100644 --- a/.github/ISSUE_TEMPLATE/installation-questions.md +++ b/.github/ISSUE_TEMPLATE/installation-questions.md @@ -11,9 +11,16 @@ assignees: '' Thank you for using cx_Oracle. +The cx_Oracle driver was renamed to python-oracledb in May 2022. It has a new +repository at https://github.com/oracle/python-oracledb. The installation +instructions are at: +https://python-oracledb.readthedocs.io/en/latest/user_guide/installation.html + Do these before creating a new issue: - Review and follow the Installation Instructions: https://cx-oracle.readthedocs.io/en/latest/user_guide/installation.html + Update to python-oracledb, if possible. + + Otherwise, review and follow the Installation Instructions: https://cx-oracle.readthedocs.io/en/latest/user_guide/installation.html Review the troubleshooting tips: https://cx-oracle.readthedocs.io/en/latest/user_guide/installation.html#troubleshooting diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 91c3b6f..25e76bf 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -1,13 +1,37 @@ -# Reporting Security Vulnerabilities +# Reporting security vulnerabilities -Oracle values the independent security research community and believes that responsible disclosure of security vulnerabilities helps us ensure the security and privacy of all our users. +Oracle values the independent security research community and believes that +responsible disclosure of security vulnerabilities helps us ensure the security +and privacy of all our users. -Please do NOT raise a GitHub Issue to report a security vulnerability. If you believe you have found a security vulnerability, please submit a report to secalert_us@oracle.com preferably with a proof of concept. We provide additional information on [how to report security vulnerabilities to Oracle](https://www.oracle.com/corporate/security-practices/assurance/vulnerability/reporting.html) which includes public encryption keys for secure email. +Please do NOT raise a GitHub Issue to report a security vulnerability. If you +believe you have found a security vulnerability, please submit a report to +[secalert_us@oracle.com][1] preferably with a proof of concept. Please review +some additional information on [how to report security vulnerabilities to +Oracle][2]. We encourage people who contact Oracle Security to use email +encryption using [our encryption key][3]. -We ask that you do not use other channels or contact project contributors directly. +We ask that you do not use other channels or contact the project maintainers +directly. -Non-vulnerability related security issues such as great new ideas for security features are welcome on GitHub Issues. +Non-vulnerability related security issues including ideas for new or improved +security features are welcome on GitHub Issues. -## Security-Related Information +## Security updates, alerts and bulletins -We will provide security related information such as a threat model, considerations for secure use, or any known security issues in our documentation. Please note that labs and sample code are intended to demonstrate a concept and may not be sufficiently hardened for production use. +Security updates will be released on a regular cadence. Many of our projects +will typically release security fixes in conjunction with the Oracle Critical +Patch Update program. Additional information, including past advisories, is +available on our [security alerts][4] page. + +## Security-related information + +We will provide security related information such as a threat model, +considerations for secure use, or any known security issues in our +documentation. Please note that labs and sample code are intended to +demonstrate a concept and may not be sufficiently hardened for production use. + +[1]: mailto:secalert_us@oracle.com +[2]: https://www.oracle.com/corporate/security-practices/assurance/vulnerability/reporting.html +[3]: https://www.oracle.com/security-alerts/encryptionkey.html +[4]: https://www.oracle.com/security-alerts/ diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md index 3037228..eeceb44 100644 --- a/.github/SUPPORT.md +++ b/.github/SUPPORT.md @@ -1,7 +1,9 @@ # Python cx_Oracle Support -cx_Oracle is an Open Source project, so do some searching and reading -before asking questions. +**The cx_Oracle driver was renamed to python-oracledb in May 2022. It has a +new repository at https://github.com/oracle/python-oracledb. Please update to +this new driver. If you still have problems, open an issue on the new +repository.** ## cx_Oracle Installation issues diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 5213271..9336c8f 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,20 +1,8 @@ Thanks for contributing! -Before submitting PRs for cx_Oracle you must have your signed *Oracle -Contributor Agreement* accepted. See https://oca.opensource.oracle.com +The cx_Oracle driver was renamed to python-oracledb in May 2022. It has a new +repository at https://github.com/oracle/python-oracledb. -If the problem solved is small, you may find it easier to open an Issue -describing the problem and its cause so we can create the fix. +Please submit your contributions to the python-oracledb repository. -The bottom of your commit message must have the following line using your name -and e-mail address as it appears in the OCA Signatories list. - -``` -Signed-off-by: Your Name -``` - -This can be automatically added to pull requests by committing with: - -``` -git commit --signoff -```` +No further releases under the cx_Oracle namespace are planned. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5010c7c..125cf13 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,44 +1,8 @@ # Contributing -We welcome your contributions! There are multiple ways to contribute. +The cx_Oracle driver was renamed to python-oracledb in May 2022. It has a new +repository at https://github.com/oracle/python-oracledb -## Issues +Please submit your contributions to the python-oracledb repository. -For bugs or enhancement requests, please file a GitHub issue unless it's security related. When filing a bug remember that the better written the bug is, the more likely it is to be fixed. If you think you've found a security vulnerability, do not raise a GitHub issue and follow the instructions on our [Security Policy](./.github/SECURITY.md). - -## Contributing Code - -We welcome your code contributions. To get started, you will need to sign the [Oracle Contributor Agreement](https://oca.opensource.oracle.com) (OCA). - -For pull requests to be accepted, the bottom of your commit message must have -the following line using the name and e-mail address you used for the OCA. - -```text -Signed-off-by: Your Name -``` - -This can be automatically added to pull requests by committing with: - -```text -git commit --signoff -``` - -Only pull requests from committers that can be verified as having -signed the OCA can be accepted. - -### Pull request process - -1. Fork this repository -1. Create a branch in your fork to implement the changes. We recommend using -the issue number as part of your branch name, e.g. `1234-fixes` -1. Ensure that any documentation is updated with the changes that are required -by your fix. -1. Ensure that any samples are updated if the base image has been changed. -1. Submit the pull request. *Do not leave the pull request blank*. Explain exactly -what your changes are meant to do and provide simple steps on how to validate -your changes. Ensure that you reference the issue you created as well. -1. We will review your PR before it is merged. - -## Code of Conduct - -Follow the [Golden Rule](https://en.wikipedia.org/wiki/Golden_Rule). If you'd like more specific guidelines see the [Contributor Covenant Code of Conduct](https://www.contributor-covenant.org/version/1/4/code-of-conduct/) +No further releases under the cx_Oracle namespace are planned. diff --git a/README.md b/README.md index 64edd71..c441549 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,17 @@ See [/test][11]. ## Contributing -See [CONTRIBUTING](https://github.com/oracle/python-cx_Oracle/blob/main/CONTRIBUTING.md) +The cx_Oracle driver was renamed to python-oracledb in May 2022. It has a new +repository at https://github.com/oracle/python-oracledb + +Please submit your contributions to the python-oracledb repository. + +No further releases under the cx_Oracle namespace are planned. + +## Security + +Please consult the [security guide](./SECURITY.md) for our responsible security +vulnerability disclosure process. ## License From 93d69d861b46962307b1fa31d1d71a38e12d459a Mon Sep 17 00:00:00 2001 From: Anthony Tuininga Date: Fri, 31 May 2024 16:47:41 -0600 Subject: [PATCH 09/13] Make samples point to python-oracledb at runtime. --- samples/README.md | 2 +- samples/sample_env.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/samples/README.md b/samples/README.md index 983b4ce..adc4dae 100644 --- a/samples/README.md +++ b/samples/README.md @@ -5,7 +5,7 @@ **cx_Oracle has a major new release under a new name and homepage [python-oracledb](https://oracle.github.io/python-oracledb/).** -**New projects should install python-oracledb instead of cx_Oracle.** +**New projects should install python-oracledb instead of cx_Oracle: python -m pip install oracledb** **The new source code and samples can be found at [github.com/oracle/python-oracledb](https://github.com/oracle/python-oracledb).** diff --git a/samples/sample_env.py b/samples/sample_env.py index 42484d3..4d87451 100644 --- a/samples/sample_env.py +++ b/samples/sample_env.py @@ -41,6 +41,19 @@ # user for on premises databases is SYSTEM. #------------------------------------------------------------------------------ +print(""" + +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +!!! cx_Oracle was renamed to python-oracledb. !!! +!!! Use python-oracledb instead of cx_Oracle for development. !!! +!!! Install with: !!! +!!! python -m pip install oracledb !!! +!!! and use the new samples in: !!! +!!! https://github.com/oracle/python-oracledb/tree/main/samples !!! +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +""") + import getpass import os import sys From bed2c037f9d7686f372fb81c589168111d7ce750 Mon Sep 17 00:00:00 2001 From: Anthony Tuininga Date: Tue, 30 Jul 2024 17:09:37 -0600 Subject: [PATCH 10/13] Relocate file per @LesiaChaban. --- .github/SECURITY.md => SECURITY.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/SECURITY.md => SECURITY.md (100%) diff --git a/.github/SECURITY.md b/SECURITY.md similarity index 100% rename from .github/SECURITY.md rename to SECURITY.md From 70cbc4375275d84323b4b462539a70ba1a8c635b Mon Sep 17 00:00:00 2001 From: Anthony Tuininga Date: Wed, 26 Mar 2025 20:19:34 -0600 Subject: [PATCH 11/13] Further nudge users towards python-oracledb. --- README.md | 96 +++++-------------- README.txt | 10 +- ...cle-Database-Scripting-for-the-Future.html | 13 ++- samples/tutorial/resources/base.css | 7 ++ 4 files changed, 47 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index c441549..98b0d82 100644 --- a/README.md +++ b/README.md @@ -1,83 +1,33 @@ # Python cx_Oracle -# News +**cx_Oracle was obsoleted by +[python-oracledb](https://oracle.github.io/python-oracledb/) in 2022.** -**cx_Oracle has a major new release under a new name and homepage -[python-oracledb](https://oracle.github.io/python-oracledb/).** +Python-oracledb uses the same Python DB API as cx_Oracle, and has many new +features. -**The source code has moved to -[github.com/oracle/python-oracledb](https://github.com/oracle/python-oracledb).** +Install with: -New projects should install python-oracledb instead of cx_Oracle. Critical -patches and binary packages for new Python releases may continue to be made in -the cx_Oracle namespace for a limited time, subject to demand. +``` +python -m pip install oracledb +``` -# About +Usage is like: -cx_Oracle is a Python extension module that enables access to Oracle -Database. It conforms to the [Python database API 2.0 -specification][1] with a considerable number of additions and a couple -of exclusions. See the -[homepage](https://oracle.github.io/python-cx_Oracle/index.html) for a -feature list. +``` +import getpass +import oracledb -cx_Oracle 8.3 was tested with Python versions 3.6 through 3.10. You can -use cx_Oracle with Oracle 11.2, 12c, 18c, 19c and 21c client libraries. -Oracle's standard client-server version interoperability allows connection to -both older and newer databases. For example Oracle 19c client libraries can -connect to Oracle Database 11.2. Older versions of cx_Oracle may work with -older versions of Python. +un = 'scott' +cs = 'localhost/orclpdb1' +pw = getpass.getpass(f'Enter password for {un}@{cs}: ') -## Installation +with oracledb.connect(user=un, password=pw, dsn=cs) as connection: + with connection.cursor() as cursor: + sql = 'select systimestamp from dual' + for r in cursor.execute(sql): + print(r) +``` -See [cx_Oracle Installation][15]. - -## Documentation - -See the [cx_Oracle Documentation][2] and [Release Notes][14]. - -## Samples - -See the [/samples][12] directory and the [tutorial][6]. You can also -look at the scripts in [cx_OracleTools][7] and the modules in -[cx_PyOracleLib][8]. - -## Help - -Issues and questions can be raised with the cx_Oracle community on -[GitHub][9] or on the [mailing list][5]. - -## Tests - -See [/test][11]. - -## Contributing - -The cx_Oracle driver was renamed to python-oracledb in May 2022. It has a new -repository at https://github.com/oracle/python-oracledb - -Please submit your contributions to the python-oracledb repository. - -No further releases under the cx_Oracle namespace are planned. - -## Security - -Please consult the [security guide](./SECURITY.md) for our responsible security -vulnerability disclosure process. - -## License - -cx_Oracle is licensed under a BSD license which you can find [here][3]. - -[1]: https://peps.python.org/pep-0249/ -[2]: https://cx-oracle.readthedocs.io -[3]: https://github.com/oracle/python-cx_Oracle/blob/main/LICENSE.txt -[5]: https://sourceforge.net/projects/cx-oracle/lists/cx-oracle-users -[6]: https://github.com/oracle/python-cx_Oracle/tree/main/samples/tutorial -[7]: http://cx-oracletools.sourceforge.net -[8]: http://cx-pyoraclelib.sourceforge.net -[9]: https://github.com/oracle/python-cx_Oracle/issues -[11]: https://github.com/oracle/python-cx_Oracle/tree/main/test -[12]: https://github.com/oracle/python-cx_Oracle/tree/main/samples -[14]: https://cx-oracle.readthedocs.io/en/latest/release_notes.html -[15]: https://cx-oracle.readthedocs.io/en/latest/user_guide/installation.html +The source code for python-oracledb is at +[github.com/oracle/python-oracledb](https://github.com/oracle/python-oracledb). diff --git a/README.txt b/README.txt index a709cfc..1313581 100644 --- a/README.txt +++ b/README.txt @@ -1,7 +1,7 @@ -An enhanced cx_Oracle release is now under the python-oracledb namespace. See -https://oracle.github.io/python-oracledb/index.html for how to install and use -this updated driver. +cx_Oracle was obsoleted by python-oracledb in 2022. -For information about cx_Oracle itself, see -https://oracle.github.io/python-cx_Oracle/index.html +Python-oracledb uses the same Python DB API as cx_Oracle, and has many new +features. +See https://python-oracledb.readthedocs.io/en/latest/index.html for how to +install and use this updated driver. diff --git a/samples/tutorial/Python-and-Oracle-Database-Scripting-for-the-Future.html b/samples/tutorial/Python-and-Oracle-Database-Scripting-for-the-Future.html index ded7787..dd677a0 100644 --- a/samples/tutorial/Python-and-Oracle-Database-Scripting-for-the-Future.html +++ b/samples/tutorial/Python-and-Oracle-Database-Scripting-for-the-Future.html @@ -11,7 +11,18 @@

Python and Oracle Database Tutorial: Scripting for the Future

- Python cx_Oracle logo +
+ +

cx_Oracle has a major new release under a new name and + homepage python-oracledb.

+ +

We recommend you use the new python-oracledb tutorial instead of this cx_Oracle tutorial.

+ +
+

Contents

diff --git a/samples/tutorial/resources/base.css b/samples/tutorial/resources/base.css index f4ff343..36128e2 100644 --- a/samples/tutorial/resources/base.css +++ b/samples/tutorial/resources/base.css @@ -60,3 +60,10 @@ hr { pre { background: #E0E0E0; } + +.announcement { + margin-top: 30px; + padding: 30px; + padding-bottom: 15px; + background-color: #F0CC71; +} From d1c8021950f4163da987cfcfe0b0af5ab1106f3a Mon Sep 17 00:00:00 2001 From: Anthony Tuininga Date: Fri, 16 May 2025 11:28:29 -0600 Subject: [PATCH 12/13] Delete samples, pointing towards python-oracledb instead. --- samples/README.md | 62 +- samples/app_context.py | 36 - samples/aq_notification.py | 46 - samples/array_dml_rowcounts.py | 48 - samples/batch_errors.py | 86 - samples/bind_insert.py | 75 - samples/bind_query.py | 31 - samples/bulk_aq.py | 75 - samples/call_timeout.py | 41 - samples/connection_pool.py | 78 - samples/cqn.py | 68 - samples/cqn2.py | 74 - samples/database_change_notification.py | 65 - samples/database_shutdown.py | 34 - samples/database_startup.py | 29 - samples/dbms_output.py | 44 - samples/dml_returning_multiple_rows.py | 44 - samples/drcp.py | 43 - samples/drop_samples.py | 24 - samples/editioning.py | 76 - samples/generic_row_factory.py | 47 - samples/implicit_results.py | 49 - samples/insert_geometry.py | 55 - samples/json_blob.py | 89 - samples/json_direct.py | 94 - samples/last_rowid.py | 56 - samples/multi_consumer_aq.py | 67 - samples/object_aq.py | 66 - samples/plsql_collection.py | 46 - samples/plsql_function.py | 18 - samples/plsql_procedure.py | 20 - samples/plsql_record.py | 46 - samples/query.py | 46 - samples/query_arraysize.py | 30 - samples/query_strings_as_bytes.py | 49 - samples/raw_aq.py | 58 - samples/ref_cursor.py | 55 - samples/return_lobs_as_strings.py | 71 - samples/return_numbers_as_decimals.py | 31 - samples/rows_as_instance.py | 57 - samples/sample_env.py | 164 - samples/scrollable_cursors.py | 70 - samples/session_callback.py | 138 - samples/session_callback_plsql.py | 102 - samples/setup_samples.py | 32 - samples/sharding_number_key.py | 35 - samples/soda_basic.py | 120 - samples/soda_bulk_insert.py | 79 - samples/spatial_to_geopandas.py | 186 -- samples/sql/drop_samples.sql | 28 - samples/sql/drop_samples_exec.sql | 32 - samples/sql/setup_samples.sql | 33 - samples/sql/setup_samples_exec.sql | 558 ---- samples/subclassing.py | 53 - samples/transaction_guard.py | 77 - ...cle-Database-Scripting-for-the-Future.html | 2693 ----------------- samples/tutorial/aq.py | 80 - samples/tutorial/bind_insert.py | 27 - samples/tutorial/bind_insert.sql | 27 - samples/tutorial/bind_query.py | 23 - samples/tutorial/bind_sdo.py | 50 - samples/tutorial/clob.py | 30 - samples/tutorial/clob_string.py | 35 - samples/tutorial/connect.py | 13 - samples/tutorial/connect_drcp.py | 14 - samples/tutorial/connect_pool.py | 34 - samples/tutorial/connect_pool2.py | 36 - samples/tutorial/db_config.py | 48 - samples/tutorial/db_config.sql | 8 - samples/tutorial/drcp_query.sql | 22 - samples/tutorial/plsql_func.py | 16 - samples/tutorial/plsql_func.sql | 35 - samples/tutorial/plsql_proc.py | 17 - samples/tutorial/plsql_proc.sql | 22 - samples/tutorial/query.py | 12 - samples/tutorial/query2.py | 19 - samples/tutorial/query_arraysize.py | 25 - samples/tutorial/query_arraysize.sql | 37 - samples/tutorial/query_many.py | 17 - samples/tutorial/query_one.py | 20 - samples/tutorial/query_scroll.py | 21 - samples/tutorial/resources/base.css | 69 - .../tutorial/resources/community-py-200.png | Bin 9040 -> 0 bytes samples/tutorial/resources/cx_Oracle_arch.png | Bin 130379 -> 0 bytes samples/tutorial/resources/favicon.ico | Bin 18094 -> 0 bytes samples/tutorial/resources/python_nopool.png | Bin 243864 -> 0 bytes samples/tutorial/resources/python_pool.png | Bin 232829 -> 0 bytes samples/tutorial/rowfactory.py | 25 - samples/tutorial/soda.py | 47 - samples/tutorial/solutions/aq-dequeue.py | 33 - samples/tutorial/solutions/aq-enqueue.py | 38 - samples/tutorial/solutions/aq-queuestart.py | 47 - samples/tutorial/solutions/bind_insert.py | 31 - samples/tutorial/solutions/bind_sdo.py | 79 - samples/tutorial/solutions/connect_pool2.py | 40 - samples/tutorial/solutions/db_config.py | 4 - samples/tutorial/solutions/query-2.py | 21 - samples/tutorial/solutions/query.py | 18 - samples/tutorial/solutions/query_many.py | 20 - samples/tutorial/solutions/query_scroll.py | 27 - samples/tutorial/solutions/rowfactory.py | 34 - samples/tutorial/solutions/soda.py | 68 - samples/tutorial/solutions/subclass.py | 39 - samples/tutorial/solutions/type_converter.py | 23 - samples/tutorial/solutions/type_output.py | 28 - samples/tutorial/solutions/versions.py | 16 - samples/tutorial/sql/create_user.sql | 59 - samples/tutorial/sql/drop_user.sql | 52 - samples/tutorial/sql/setup_tables.sql | 111 - samples/tutorial/subclass.py | 23 - samples/tutorial/type_converter.py | 16 - samples/tutorial/type_input.py | 85 - samples/tutorial/type_output.py | 17 - samples/tutorial/versions.py | 14 - samples/type_handlers.py | 92 - samples/universal_rowids.py | 57 - 116 files changed, 9 insertions(+), 8441 deletions(-) delete mode 100644 samples/app_context.py delete mode 100644 samples/aq_notification.py delete mode 100644 samples/array_dml_rowcounts.py delete mode 100644 samples/batch_errors.py delete mode 100644 samples/bind_insert.py delete mode 100644 samples/bind_query.py delete mode 100644 samples/bulk_aq.py delete mode 100644 samples/call_timeout.py delete mode 100644 samples/connection_pool.py delete mode 100644 samples/cqn.py delete mode 100644 samples/cqn2.py delete mode 100644 samples/database_change_notification.py delete mode 100644 samples/database_shutdown.py delete mode 100644 samples/database_startup.py delete mode 100644 samples/dbms_output.py delete mode 100644 samples/dml_returning_multiple_rows.py delete mode 100644 samples/drcp.py delete mode 100644 samples/drop_samples.py delete mode 100644 samples/editioning.py delete mode 100644 samples/generic_row_factory.py delete mode 100644 samples/implicit_results.py delete mode 100644 samples/insert_geometry.py delete mode 100644 samples/json_blob.py delete mode 100644 samples/json_direct.py delete mode 100644 samples/last_rowid.py delete mode 100644 samples/multi_consumer_aq.py delete mode 100644 samples/object_aq.py delete mode 100644 samples/plsql_collection.py delete mode 100644 samples/plsql_function.py delete mode 100644 samples/plsql_procedure.py delete mode 100644 samples/plsql_record.py delete mode 100644 samples/query.py delete mode 100644 samples/query_arraysize.py delete mode 100644 samples/query_strings_as_bytes.py delete mode 100644 samples/raw_aq.py delete mode 100644 samples/ref_cursor.py delete mode 100644 samples/return_lobs_as_strings.py delete mode 100644 samples/return_numbers_as_decimals.py delete mode 100644 samples/rows_as_instance.py delete mode 100644 samples/sample_env.py delete mode 100644 samples/scrollable_cursors.py delete mode 100644 samples/session_callback.py delete mode 100644 samples/session_callback_plsql.py delete mode 100644 samples/setup_samples.py delete mode 100644 samples/sharding_number_key.py delete mode 100644 samples/soda_basic.py delete mode 100644 samples/soda_bulk_insert.py delete mode 100644 samples/spatial_to_geopandas.py delete mode 100644 samples/sql/drop_samples.sql delete mode 100644 samples/sql/drop_samples_exec.sql delete mode 100644 samples/sql/setup_samples.sql delete mode 100644 samples/sql/setup_samples_exec.sql delete mode 100644 samples/subclassing.py delete mode 100644 samples/transaction_guard.py delete mode 100644 samples/tutorial/Python-and-Oracle-Database-Scripting-for-the-Future.html delete mode 100644 samples/tutorial/aq.py delete mode 100644 samples/tutorial/bind_insert.py delete mode 100644 samples/tutorial/bind_insert.sql delete mode 100644 samples/tutorial/bind_query.py delete mode 100644 samples/tutorial/bind_sdo.py delete mode 100644 samples/tutorial/clob.py delete mode 100644 samples/tutorial/clob_string.py delete mode 100644 samples/tutorial/connect.py delete mode 100644 samples/tutorial/connect_drcp.py delete mode 100644 samples/tutorial/connect_pool.py delete mode 100644 samples/tutorial/connect_pool2.py delete mode 100644 samples/tutorial/db_config.py delete mode 100644 samples/tutorial/db_config.sql delete mode 100644 samples/tutorial/drcp_query.sql delete mode 100644 samples/tutorial/plsql_func.py delete mode 100644 samples/tutorial/plsql_func.sql delete mode 100644 samples/tutorial/plsql_proc.py delete mode 100644 samples/tutorial/plsql_proc.sql delete mode 100644 samples/tutorial/query.py delete mode 100644 samples/tutorial/query2.py delete mode 100644 samples/tutorial/query_arraysize.py delete mode 100644 samples/tutorial/query_arraysize.sql delete mode 100644 samples/tutorial/query_many.py delete mode 100644 samples/tutorial/query_one.py delete mode 100644 samples/tutorial/query_scroll.py delete mode 100644 samples/tutorial/resources/base.css delete mode 100644 samples/tutorial/resources/community-py-200.png delete mode 100644 samples/tutorial/resources/cx_Oracle_arch.png delete mode 100644 samples/tutorial/resources/favicon.ico delete mode 100644 samples/tutorial/resources/python_nopool.png delete mode 100644 samples/tutorial/resources/python_pool.png delete mode 100644 samples/tutorial/rowfactory.py delete mode 100644 samples/tutorial/soda.py delete mode 100644 samples/tutorial/solutions/aq-dequeue.py delete mode 100644 samples/tutorial/solutions/aq-enqueue.py delete mode 100644 samples/tutorial/solutions/aq-queuestart.py delete mode 100644 samples/tutorial/solutions/bind_insert.py delete mode 100644 samples/tutorial/solutions/bind_sdo.py delete mode 100644 samples/tutorial/solutions/connect_pool2.py delete mode 100644 samples/tutorial/solutions/db_config.py delete mode 100644 samples/tutorial/solutions/query-2.py delete mode 100644 samples/tutorial/solutions/query.py delete mode 100644 samples/tutorial/solutions/query_many.py delete mode 100644 samples/tutorial/solutions/query_scroll.py delete mode 100644 samples/tutorial/solutions/rowfactory.py delete mode 100644 samples/tutorial/solutions/soda.py delete mode 100644 samples/tutorial/solutions/subclass.py delete mode 100644 samples/tutorial/solutions/type_converter.py delete mode 100644 samples/tutorial/solutions/type_output.py delete mode 100644 samples/tutorial/solutions/versions.py delete mode 100644 samples/tutorial/sql/create_user.sql delete mode 100644 samples/tutorial/sql/drop_user.sql delete mode 100644 samples/tutorial/sql/setup_tables.sql delete mode 100644 samples/tutorial/subclass.py delete mode 100644 samples/tutorial/type_converter.py delete mode 100644 samples/tutorial/type_input.py delete mode 100644 samples/tutorial/type_output.py delete mode 100644 samples/tutorial/versions.py delete mode 100644 samples/type_handlers.py delete mode 100644 samples/universal_rowids.py diff --git a/samples/README.md b/samples/README.md index adc4dae..ea0bb1c 100644 --- a/samples/README.md +++ b/samples/README.md @@ -1,59 +1,15 @@ # Samples -## News - **cx_Oracle has a major new release under a new name and homepage -[python-oracledb](https://oracle.github.io/python-oracledb/).** - -**New projects should install python-oracledb instead of cx_Oracle: python -m pip install oracledb** - -**The new source code and samples can be found at -[github.com/oracle/python-oracledb](https://github.com/oracle/python-oracledb).** - -## cx_Oracle Examples - -This directory contains samples for [cx_Oracle][6]. Documentation is -[here][7]. A separate tutorial is [here][8]. - -1. The schemas and SQL objects that are referenced in the samples can be - created by running the Python script [setup_samples.py][1]. The script - requires SYSDBA privileges and will prompt for these credentials as well as - the names of the schemas and edition that will be created, unless a number - of environment variables are set as documented in the Python script - [sample_env.py][2]. Run the script using the following command: - - python setup_samples.py - - Alternatively, the [SQL script][3] can be run directly via SQL\*Plus, which - will always prompt for the names of the schemas and edition that will be - created. - - sqlplus sys/syspassword@hostname/servicename @sql/setup_samples.sql - -2. Run a Python script, for example: - - python query.py - -3. After running cx_Oracle samples, the schemas and SQL objects can be - dropped by running the Python script [drop_samples.py][4]. The script - requires SYSDBA privileges and will prompt for these credentials as well as - the names of the schemas and edition that will be dropped, unless a number - of environment variables are set as documented in the Python script - [sample_env.py][2]. Run the script using the following command: - - python drop_samples.py +[python-oracledb](https://oracle.github.io/python-oracledb/). New projects +should use python-oracledb instead of the obsolete cx_Oracle driver.** - Alternatively, the [SQL script][5] can be run directly via SQL\*Plus, which - will always prompt for the names of the schemas and edition that will be - dropped. +Python-oracledb uses the same Python Database API as cx_Oracle, supports the +feature requirements of frameworks that rely on this API, and has many new +features. - sqlplus sys/syspassword@hostname/servicename @sql/drop_samples.sql +**Python-oracledb samples can be found at +[github.com/oracle/python-oracledb/tree/main/samples](https://github.com/oracle/python-oracledb/tree/main/samples).** -[1]: https://github.com/oracle/python-cx_Oracle/blob/main/samples/setup_samples.py -[2]: https://github.com/oracle/python-cx_Oracle/blob/main/samples/sample_env.py -[3]: https://github.com/oracle/python-cx_Oracle/blob/main/samples/sql/setup_samples.sql -[4]: https://github.com/oracle/python-cx_Oracle/blob/main/samples/drop_samples.py -[5]: https://github.com/oracle/python-cx_Oracle/blob/main/samples/sql/drop_samples.sql -[6]: https://oracle.github.io/python-cx_Oracle/ -[7]: http://cx-oracle.readthedocs.org/en/latest/index.html -[8]: https://oracle.github.io/python-cx_Oracle/samples/tutorial/Python-and-Oracle-Database-Scripting-for-the-Future.html +To upgrade to python-oracledb, see [Upgrading from cx_Oracle 8.3 to +python-oracledb](https://python-oracledb.readthedocs.io/en/latest/user_guide/appendix_c.html#upgrading-from-cx-oracle-8-3-to-python-oracledb). diff --git a/samples/app_context.py b/samples/app_context.py deleted file mode 100644 index 41c411f..0000000 --- a/samples/app_context.py +++ /dev/null @@ -1,36 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved. -# -# Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. -# -# Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, -# Canada. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# app_context.py -# This script demonstrates the use of application context. Application -# context is available within logon triggers and can be retrieved by using the -# function sys_context(). -# -# This script requires cx_Oracle 5.3 and higher. -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb -import sample_env - -# define constants used throughout the script; adjust as desired -APP_CTX_NAMESPACE = "CLIENTCONTEXT" -APP_CTX_ENTRIES = [ - ( APP_CTX_NAMESPACE, "ATTR1", "VALUE1" ), - ( APP_CTX_NAMESPACE, "ATTR2", "VALUE2" ), - ( APP_CTX_NAMESPACE, "ATTR3", "VALUE3" ) -] - -connection = oracledb.connect(sample_env.get_main_connect_string(), - appcontext=APP_CTX_ENTRIES) -cursor = connection.cursor() -for namespace, name, value in APP_CTX_ENTRIES: - cursor.execute("select sys_context(:1, :2) from dual", (namespace, name)) - value, = cursor.fetchone() - print("Value of context key", name, "is", value) diff --git a/samples/aq_notification.py b/samples/aq_notification.py deleted file mode 100644 index 728d5b2..0000000 --- a/samples/aq_notification.py +++ /dev/null @@ -1,46 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2018, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# aq_notification.py -# This script demonstrates using advanced queuing notification. Once this -# script is running, use another session to enqueue a few messages to the -# "DEMO_BOOK_QUEUE" queue. This is most easily accomplished by running the -# object_aq.py sample. -# -# This script requires cx_Oracle 6.4 and higher. -#------------------------------------------------------------------------------ - -import time - -import cx_Oracle as oracledb -import sample_env - -registered = True - -def process_messages(message): - global registered - print("Message type:", message.type) - if message.type == oracledb.EVENT_DEREG: - print("Deregistration has taken place...") - registered = False - return - print("Queue name:", message.queueName) - print("Consumer name:", message.consumerName) - -connection = oracledb.connect(sample_env.get_main_connect_string(), - events=True) -sub = connection.subscribe(namespace=oracledb.SUBSCR_NAMESPACE_AQ, - name="DEMO_BOOK_QUEUE", callback=process_messages, - timeout=300) -print("Subscription:", sub) -print("--> Connection:", sub.connection) -print("--> Callback:", sub.callback) -print("--> Namespace:", sub.namespace) -print("--> Protocol:", sub.protocol) -print("--> Timeout:", sub.timeout) - -while registered: - print("Waiting for notifications....") - time.sleep(5) diff --git a/samples/array_dml_rowcounts.py b/samples/array_dml_rowcounts.py deleted file mode 100644 index 335d46c..0000000 --- a/samples/array_dml_rowcounts.py +++ /dev/null @@ -1,48 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# array_dml_rowcounts.py -# -# Demonstrate the use of the 12.1 feature that allows cursor.executemany() -# to return the number of rows affected by each individual execution as a list. -# The parameter "arraydmlrowcounts" must be set to True in the call to -# cursor.executemany() after which cursor.getarraydmlrowcounts() can be called. -# -# This script requires cx_Oracle 5.2 and higher. -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb -import sample_env - -connection = oracledb.connect(sample_env.get_main_connect_string()) -cursor = connection.cursor() - -# show the number of rows for each parent ID as a means of verifying the -# output from the delete statement -for parent_id, count in cursor.execute(""" - select ParentId, count(*) - from ChildTable - group by ParentId - order by ParentId"""): - print("Parent ID:", parent_id, "has", int(count), "rows.") -print() - -# delete the following parent IDs only -parent_ids_to_delete = [20, 30, 50] - -print("Deleting Parent IDs:", parent_ids_to_delete) -print() - -# enable array DML row counts for each iteration executed in executemany() -cursor.executemany(""" - delete from ChildTable - where ParentId = :1""", - [(i,) for i in parent_ids_to_delete], - arraydmlrowcounts = True) - -# display the number of rows deleted for each parent ID -row_counts = cursor.getarraydmlrowcounts() -for parent_id, count in zip(parent_ids_to_delete, row_counts): - print("Parent ID:", parent_id, "deleted", count, "rows.") diff --git a/samples/batch_errors.py b/samples/batch_errors.py deleted file mode 100644 index 8d093bd..0000000 --- a/samples/batch_errors.py +++ /dev/null @@ -1,86 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# batch_errors.py -# -# Demonstrate the use of the Oracle Database 12.1 feature that allows -# cursor.executemany() to complete successfully, even if errors take -# place during the execution of one or more of the individual -# executions. The parameter "batcherrors" must be set to True in the -# call to cursor.executemany() after which cursor.getbatcherrors() can -# be called, which will return a list of error objects. -# -# This script requires cx_Oracle 5.2 and higher. -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb -import sample_env - -connection = oracledb.connect(sample_env.get_main_connect_string()) -cursor = connection.cursor() - -# define data to insert -data_to_insert = [ - (1016, 10, 'Child B of Parent 10'), - (1017, 10, 'Child C of Parent 10'), - (1018, 20, 'Child D of Parent 20'), - (1018, 20, 'Child D of Parent 20'), # duplicate key - (1019, 30, 'Child C of Parent 30'), - (1020, 30, 'Child D of Parent 40'), - (1021, 60, 'Child A of Parent 60'), # parent does not exist - (1022, 40, 'Child F of Parent 40'), -] - -# retrieve the number of rows in the table -cursor.execute(""" - select count(*) - from ChildTable""") -count, = cursor.fetchone() -print("number of rows in child table:", int(count)) -print("number of rows to insert:", len(data_to_insert)) - -# old method: executemany() with data errors results in stoppage after the -# first error takes place; the row count is updated to show how many rows -# actually succeeded -try: - cursor.executemany("insert into ChildTable values (:1, :2, :3)", - data_to_insert) -except oracledb.DatabaseError as e: - error, = e.args - print("FAILED with error:", error.message) - print("number of rows which succeeded:", cursor.rowcount) - -# demonstrate that the row count is accurate -cursor.execute(""" - select count(*) - from ChildTable""") -count, = cursor.fetchone() -print("number of rows in child table after failed insert:", int(count)) - -# roll back so we can perform the same work using the new method -connection.rollback() - -# new method: executemany() with batch errors enabled (and array DML row counts -# also enabled) results in no immediate error being raised -cursor.executemany("insert into ChildTable values (:1, :2, :3)", - data_to_insert, batcherrors=True, arraydmlrowcounts=True) - -# where errors have taken place, the row count is 0; otherwise it is 1 -row_counts = cursor.getarraydmlrowcounts() -print("Array DML row counts:", row_counts) - -# display the errors that have taken place -errors = cursor.getbatcherrors() -print("number of errors which took place:", len(errors)) -for error in errors: - print("Error", error.message.rstrip(), "at row offset", error.offset) - -# demonstrate that all of the rows without errors have been successfully -# inserted -cursor.execute(""" - select count(*) - from ChildTable""") -count, = cursor.fetchone() -print("number of rows in child table after successful insert:", int(count)) diff --git a/samples/bind_insert.py b/samples/bind_insert.py deleted file mode 100644 index fef635b..0000000 --- a/samples/bind_insert.py +++ /dev/null @@ -1,75 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# bind_insert.py -# -# Demonstrate how to insert a row into a table using bind variables. -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb -import sample_env - -connection = oracledb.connect(sample_env.get_main_connect_string()) - -#------------------------------------------------------------------------------ -# "Bind by position" -#------------------------------------------------------------------------------ - -rows = [ - (1, "First"), - (2, "Second"), - (3, "Third"), - (4, "Fourth"), - (5, None), # Insert a NULL value - (6, "Sixth"), - (7, "Seventh") -] - -cursor = connection.cursor() - -# predefine maximum string size to avoid data scans and memory reallocations; -# the None value indicates that the default processing can take place -cursor.setinputsizes(None, 20) - -cursor.executemany("insert into mytab(id, data) values (:1, :2)", rows) - -#------------------------------------------------------------------------------ -# "Bind by name" -#------------------------------------------------------------------------------ - -rows = [ - {"d": "Eighth", "i": 8}, - {"d": "Ninth", "i": 9}, - {"d": "Tenth", "i": 10} -] - -cursor = connection.cursor() - -# Predefine maximum string size to avoid data scans and memory reallocations -cursor.setinputsizes(d=20) - -cursor.executemany("insert into mytab(id, data) values (:i, :d)", rows) - -#------------------------------------------------------------------------------ -# Inserting a single bind still needs tuples -#------------------------------------------------------------------------------ - -rows = [ - ("Eleventh",), - ("Twelth",) -] - -cursor = connection.cursor() -cursor.executemany("insert into mytab(id, data) values (11, :1)", rows) - -#------------------------------------------------------------------------------ -# Now query the results back -#------------------------------------------------------------------------------ - -# Don't commit - this lets the demo be run multiple times -#connection.commit() - -for row in cursor.execute('select * from mytab'): - print(row) diff --git a/samples/bind_query.py b/samples/bind_query.py deleted file mode 100644 index 5b09bc7..0000000 --- a/samples/bind_query.py +++ /dev/null @@ -1,31 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# bind_query.py -# -# Demonstrate how to perform a simple query limiting the rows retrieved using -# a bind variable. Since the query that is executed is identical, no additional -# parsing is required, thereby reducing overhead and increasing performance. It -# also permits data to be bound without having to be concerned about escaping -# special characters or SQL injection attacks. -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb -import sample_env - -connection = oracledb.connect(sample_env.get_main_connect_string()) - -cursor = connection.cursor() -sql = 'select * from SampleQueryTab where id = :bvid' - -print("Query results with id = 4") -for row in cursor.execute(sql, bvid = 4): - print(row) -print() - -print("Query results with id = 1") -for row in cursor.execute(sql, bvid = 1): - print(row) -print() diff --git a/samples/bulk_aq.py b/samples/bulk_aq.py deleted file mode 100644 index 07320e8..0000000 --- a/samples/bulk_aq.py +++ /dev/null @@ -1,75 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2019, 2021, Oracle and/or its affiliates. All rights reserved. -# -# Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. -# -# Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, -# Canada. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# bulk_aq.py -# This script demonstrates how to use bulk enqueuing and dequeuing of -# messages with advanced queuing. It makes use of a RAW queue created in the -# sample setup. -# -# This script requires cx_Oracle 8.2 and higher. -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb -import sample_env - -QUEUE_NAME = "DEMO_RAW_QUEUE" -PAYLOAD_DATA = [ - "The first message", - "The second message", - "The third message", - "The fourth message", - "The fifth message", - "The sixth message", - "The seventh message", - "The eighth message", - "The ninth message", - "The tenth message", - "The eleventh message", - "The twelfth and final message" -] - -# connect to database -connection = oracledb.connect(sample_env.get_main_connect_string()) -cursor = connection.cursor() - -# create queue -queue = connection.queue(QUEUE_NAME) -queue.deqoptions.wait = oracledb.DEQ_NO_WAIT -queue.deqoptions.navigation = oracledb.DEQ_FIRST_MSG - -# dequeue all existing messages to ensure the queue is empty, just so that -# the results are consistent -while queue.deqone(): - pass - -# enqueue a few messages -print("Enqueuing messages...") -batch_size = 6 -data_to_enqueue = PAYLOAD_DATA -while data_to_enqueue: - batch_data = data_to_enqueue[:batch_size] - data_to_enqueue = data_to_enqueue[batch_size:] - messages = [connection.msgproperties(payload=d) for d in batch_data] - for data in batch_data: - print(data) - queue.enqmany(messages) -connection.commit() - -# dequeue the messages -print("\nDequeuing messages...") -batch_size = 8 -while True: - messages = queue.deqmany(batch_size) - if not messages: - break - for props in messages: - print(props.payload.decode()) -connection.commit() -print("\nDone.") diff --git a/samples/call_timeout.py b/samples/call_timeout.py deleted file mode 100644 index b665d9f..0000000 --- a/samples/call_timeout.py +++ /dev/null @@ -1,41 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2019, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# call_timeout.py -# -# Demonstrate the use of the Oracle Client 18c feature that enables round trips -# to the database to time out if a specified amount of time (in milliseconds) -# has passed without a response from the database. -# -# This script requires cx_Oracle 7.0 and higher and Oracle Client 18.1 and -# higher. -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb -import sample_env - -connection = oracledb.connect(sample_env.get_main_connect_string()) -connection.call_timeout = 2000 -print("Call timeout set at", connection.call_timeout, "milliseconds...") - -cursor = connection.cursor() -cursor.execute("select sysdate from dual") -today, = cursor.fetchone() -print("Fetch of current date before timeout:", today) - -# dbms_session.sleep() replaces dbms_lock.sleep() from Oracle Database 18c -sleep_proc_name = "dbms_session.sleep" \ - if int(connection.version.split(".")[0]) >= 18 \ - else "dbms_lock.sleep" - -print("Sleeping...should time out...") -try: - cursor.callproc(sleep_proc_name, (3,)) -except oracledb.DatabaseError as e: - print("ERROR:", e) - -cursor.execute("select sysdate from dual") -today, = cursor.fetchone() -print("Fetch of current date after timeout:", today) diff --git a/samples/connection_pool.py b/samples/connection_pool.py deleted file mode 100644 index 348712b..0000000 --- a/samples/connection_pool.py +++ /dev/null @@ -1,78 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# connection_pool.py -# This script demonstrates the use of connection pooling. Pools can -# significantly reduce connection times for long running applications that -# repeatedly open and close connections. Internal features help protect against -# dead connections, and also aid use of Oracle Database features such as FAN -# and Application Continuity. -# The script uses threading to show multiple users of the pool. One thread -# performs a database sleep while another performs a query. A more typical -# application might be a web service that handles requests from multiple users. -# Note only one operation (such as an execute or fetch) can take place at a time -# on each connection. -# -# Also see session_callback.py. -# -#------------------------------------------------------------------------------ - -import threading - -import cx_Oracle as oracledb -import sample_env - -# Create a Connection Pool -pool = oracledb.SessionPool(user=sample_env.get_main_user(), - password=sample_env.get_main_password(), - dsn=sample_env.get_connect_string(), min=2, max=5, - increment=1) - -def the_long_query(): - with pool.acquire() as conn: - cursor = conn.cursor() - cursor.arraysize = 25000 - print("the_long_query(): beginning execute...") - cursor.execute(""" - select * - from - TestNumbers - cross join TestNumbers - cross join TestNumbers - cross join TestNumbers - cross join TestNumbers - cross join TestNumbers""") - print("the_long_query(): done execute...") - while True: - rows = cursor.fetchmany() - if not rows: - break - print("the_long_query(): fetched", len(rows), "rows...") - print("the_long_query(): all done!") - - -def do_a_lock(): - with pool.acquire() as conn: - # dbms_session.sleep() replaces dbms_lock.sleep() - # from Oracle Database 18c - sleep_proc_name = "dbms_session.sleep" \ - if int(conn.version.split(".")[0]) >= 18 \ - else "dbms_lock.sleep" - cursor = conn.cursor() - print("do_a_lock(): beginning execute...") - cursor.callproc(sleep_proc_name, (5,)) - print("do_a_lock(): done execute...") - - -thread1 = threading.Thread(target=the_long_query) -thread1.start() - -thread2 = threading.Thread(target=do_a_lock) -thread2.start() - -thread1.join() -thread2.join() - -print("All done!") diff --git a/samples/cqn.py b/samples/cqn.py deleted file mode 100644 index 5a055ca..0000000 --- a/samples/cqn.py +++ /dev/null @@ -1,68 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved. -# -# Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. -# -# Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, -# Canada. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# cqn.py -# This script demonstrates using continuous query notification in Python, a -# feature that is available in Oracle 11g and later. Once this script is -# running, use another session to insert, update or delete rows from the table -# TestTempTable and you will see the notification of that change. -# -# This script requires cx_Oracle 5.3 and higher. -#------------------------------------------------------------------------------ - -import time - -import cx_Oracle as oracledb -import sample_env - -registered = True - -def callback(message): - global registered - print("Message type:", message.type) - if not message.registered: - print("Deregistration has taken place...") - registered = False - return - print("Message database name:", message.dbname) - print("Message tranasction id:", message.txid) - print("Message queries:") - for query in message.queries: - print("--> Query ID:", query.id) - print("--> Query Operation:", query.operation) - for table in query.tables: - print("--> --> Table Name:", table.name) - print("--> --> Table Operation:", table.operation) - if table.rows is not None: - print("--> --> Table Rows:") - for row in table.rows: - print("--> --> --> Row RowId:", row.rowid) - print("--> --> --> Row Operation:", row.operation) - print("-" * 60) - print("=" * 60) - -connection = oracledb.connect(sample_env.get_main_connect_string(), - events=True) -qos = oracledb.SUBSCR_QOS_QUERY | oracledb.SUBSCR_QOS_ROWIDS -sub = connection.subscribe(callback=callback, timeout=1800, qos=qos) -print("Subscription:", sub) -print("--> Connection:", sub.connection) -print("--> Callback:", sub.callback) -print("--> Namespace:", sub.namespace) -print("--> Protocol:", sub.protocol) -print("--> Timeout:", sub.timeout) -print("--> Operations:", sub.operations) -print("--> Rowids?:", bool(sub.qos & oracledb.SUBSCR_QOS_ROWIDS)) -query_id = sub.registerquery("select * from TestTempTable") -print("Registered query:", query_id) - -while registered: - print("Waiting for notifications....") - time.sleep(5) diff --git a/samples/cqn2.py b/samples/cqn2.py deleted file mode 100644 index 49fe1a5..0000000 --- a/samples/cqn2.py +++ /dev/null @@ -1,74 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# cqn2.py -# This script demonstrates using continuous query notification in Python, a -# feature that is available in Oracle 11g and later. Once this script is -# running, use another session to insert, update or delete rows from the table -# TestTempTable and you will see the notification of that change. -# -# This script differs from cqn.py in that it shows how a connection can be -# acquired from a session pool and used to query the changes that have been -# made. -# -# This script requires cx_Oracle 7 or higher. -#------------------------------------------------------------------------------ - -import time - -import cx_Oracle as oracledb -import sample_env - -registered = True - -def callback(message): - global registered - if not message.registered: - print("Deregistration has taken place...") - registered = False - return - connection = pool.acquire() - for query in message.queries: - for table in query.tables: - if table.rows is None: - print("Too many row changes detected in table", table.name) - continue - num_rows_deleted = 0 - print(len(table.rows), "row changes detected in table", table.name) - for row in table.rows: - if row.operation & oracledb.OPCODE_DELETE: - num_rows_deleted += 1 - continue - ops = [] - if row.operation & oracledb.OPCODE_INSERT: - ops.append("inserted") - if row.operation & oracledb.OPCODE_UPDATE: - ops.append("updated") - cursor = connection.cursor() - cursor.execute(""" - select IntCol - from TestTempTable - where rowid = :rid""", - rid=row.rowid) - int_col, = cursor.fetchone() - print(" Row with IntCol", int_col, "was", " and ".join(ops)) - if num_rows_deleted > 0: - print(" ", num_rows_deleted, "rows deleted") - print("=" * 60) - -pool = oracledb.SessionPool(user=sample_env.get_main_user(), - password=sample_env.get_main_password(), - dsn=sample_env.get_connect_string(), min=2, max=5, - increment=1, events=True) -with pool.acquire() as connection: - qos = oracledb.SUBSCR_QOS_QUERY | oracledb.SUBSCR_QOS_ROWIDS - sub = connection.subscribe(callback=callback, timeout=1800, qos=qos) - print("Subscription created with ID:", sub.id) - query_id = sub.registerquery("select * from TestTempTable") - print("Registered query with ID:", query_id) - -while registered: - print("Waiting for notifications....") - time.sleep(5) diff --git a/samples/database_change_notification.py b/samples/database_change_notification.py deleted file mode 100644 index 012fc63..0000000 --- a/samples/database_change_notification.py +++ /dev/null @@ -1,65 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved. -# -# Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. -# -# Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, -# Canada. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# database_change_notification.py -# This script demonstrates using database change notification in Python, a -# feature that is available in Oracle 10g Release 2. Once this script is -# running, use another session to insert, update or delete rows from the table -# TestTempTable and you will see the notification of that change. -# -# This script requires cx_Oracle 5.3 and higher. -#------------------------------------------------------------------------------ - -import time - -import cx_Oracle as oracledb -import sample_env - -registered = True - -def callback(message): - global registered - print("Message type:", message.type) - if not message.registered: - print("Deregistration has taken place...") - registered = False - return - print("Message database name:", message.dbname) - print("Message tranasction id:", message.txid) - print("Message tables:") - for table in message.tables: - print("--> Table Name:", table.name) - print("--> Table Operation:", table.operation) - if table.rows is not None: - print("--> Table Rows:") - for row in table.rows: - print("--> --> Row RowId:", row.rowid) - print("--> --> Row Operation:", row.operation) - print("-" * 60) - print("=" * 60) - -connection = oracledb.connect(sample_env.get_main_connect_string(), - events=True) -sub = connection.subscribe(callback=callback, timeout=1800, - qos=oracledb.SUBSCR_QOS_ROWIDS) -print("Subscription:", sub) -print("--> Connection:", sub.connection) -print("--> ID:", sub.id) -print("--> Callback:", sub.callback) -print("--> Namespace:", sub.namespace) -print("--> Protocol:", sub.protocol) -print("--> Timeout:", sub.timeout) -print("--> Operations:", sub.operations) -print("--> Rowids?:", bool(sub.qos & oracledb.SUBSCR_QOS_ROWIDS)) -sub.registerquery("select * from TestTempTable") - -while registered: - print("Waiting for notifications....") - time.sleep(5) diff --git a/samples/database_shutdown.py b/samples/database_shutdown.py deleted file mode 100644 index 63210b5..0000000 --- a/samples/database_shutdown.py +++ /dev/null @@ -1,34 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved. -# -# Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. -# -# Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, -# Canada. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# database_shutdown.py -# This script demonstrates shutting down a database using Python. The -# connection used assumes that the environment variable ORACLE_SID has been -# set. -# -# This script requires cx_Oracle 4.3 and higher. -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb - -# need to connect as SYSDBA or SYSOPER -connection = oracledb.connect(mode=oracledb.SYSDBA) - -# first shutdown() call must specify the mode, if DBSHUTDOWN_ABORT is used, -# there is no need for any of the other steps -connection.shutdown(mode=oracledb.DBSHUTDOWN_IMMEDIATE) - -# now close and dismount the database -cursor = connection.cursor() -cursor.execute("alter database close normal") -cursor.execute("alter database dismount") - -# perform the final shutdown call -connection.shutdown(mode=oracledb.DBSHUTDOWN_FINAL) diff --git a/samples/database_startup.py b/samples/database_startup.py deleted file mode 100644 index e036458..0000000 --- a/samples/database_startup.py +++ /dev/null @@ -1,29 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved. -# -# Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. -# -# Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, -# Canada. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# database_startup.py -# This script demonstrates starting up a database using Python. The -# connection used assumes that the environment variable ORACLE_SID has been -# set. -# -# This script requires cx_Oracle 4.3 and higher. -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb - -# the connection must be in PRELIM_AUTH mode -connection = oracledb.connect(mode=oracledb.SYSDBA | oracledb.PRELIM_AUTH) -connection.startup() - -# the following statements must be issued in normal SYSDBA mode -connection = oracledb.connect("/", mode=oracledb.SYSDBA) -cursor = connection.cursor() -cursor.execute("alter database mount") -cursor.execute("alter database open") diff --git a/samples/dbms_output.py b/samples/dbms_output.py deleted file mode 100644 index e16ad0d..0000000 --- a/samples/dbms_output.py +++ /dev/null @@ -1,44 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# dbms_output.py -# This script demonstrates one method of fetching the lines produced by -# the DBMS_OUTPUT package. -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb -import sample_env - -connection = oracledb.connect(sample_env.get_main_connect_string()) -cursor = connection.cursor() - -# enable DBMS_OUTPUT -cursor.callproc("dbms_output.enable") - -# execute some PL/SQL that generates output with DBMS_OUTPUT.PUT_LINE -cursor.execute(""" - begin - dbms_output.put_line('This is the cx_Oracle manual'); - dbms_output.put_line(''); - dbms_output.put_line('Demonstrating use of DBMS_OUTPUT'); - end;""") - -# tune this size for your application -chunk_size = 10 - -# create variables to hold the output -lines_var = cursor.arrayvar(str, chunk_size) -num_lines_var = cursor.var(int) -num_lines_var.setvalue(0, chunk_size) - -# fetch the text that was added by PL/SQL -while True: - cursor.callproc("dbms_output.get_lines", (lines_var, num_lines_var)) - num_lines = num_lines_var.getvalue() - lines = lines_var.getvalue()[:num_lines] - for line in lines: - print(line or "") - if num_lines < chunk_size: - break diff --git a/samples/dml_returning_multiple_rows.py b/samples/dml_returning_multiple_rows.py deleted file mode 100644 index cd11c2d..0000000 --- a/samples/dml_returning_multiple_rows.py +++ /dev/null @@ -1,44 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. -# -# Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. -# -# Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, -# Canada. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# dml_returning_multiple_rows.py -# This script demonstrates the use of DML returning with multiple rows being -# returned at once. -# -# This script requires cx_Oracle 6.0 and higher. -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb -import sample_env - -# truncate table first so that script can be rerun -connection = oracledb.connect(sample_env.get_main_connect_string()) -cursor = connection.cursor() -print("Truncating table...") -cursor.execute("truncate table TestTempTable") - -# populate table with a few rows -for i in range(5): - data = (i + 1, "Test String #%d" % (i + 1)) - print("Adding row", data) - cursor.execute("insert into TestTempTable values (:1, :2)", data) - -# now delete them and use DML returning to return the data that was inserted -int_col = cursor.var(int) -string_col = cursor.var(str) -print("Deleting data with DML returning...") -cursor.execute(""" - delete from TestTempTable - returning IntCol, StringCol into :int_col, :string_col""", - int_col=int_col, - string_col=string_col) -print("Data returned:") -for int_val, string_val in zip(int_col.getvalue(), string_col.getvalue()): - print(tuple([int_val, string_val])) diff --git a/samples/drcp.py b/samples/drcp.py deleted file mode 100644 index 289b856..0000000 --- a/samples/drcp.py +++ /dev/null @@ -1,43 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved. -# -# Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. -# -# Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, -# Canada. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# drcp.py -# This script demonstrates the use of Database Resident Connection Pooling -# (DRCP) which provides a connection pool in the database server, thereby -# reducing the cost of creating and tearing down client connections. The pool -# can be started and stopped in the database by issuing the following commands -# in SQL*Plus: -# -# exec dbms_connection_pool.start_pool() -# exec dbms_connection_pool.stop_pool() -# -# Statistics regarding the pool can be acquired from the following query: -# -# select * from v$cpool_cc_stats; -# -# There is no difference in how a connection is used once it has been -# established. -# -# DRCP has most benefit when used in conjunction with cx_Oracle's local -# connection pool, see the cx_Oracle documentation. -# -# This script requires cx_Oracle 5.0 or higher. -# -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb -import sample_env - -conn = oracledb.connect(sample_env.get_drcp_connect_string(), - cclass="PYCLASS", purity=oracledb.ATTR_PURITY_SELF) -cursor = conn.cursor() -print("Performing query using DRCP...") -for row in cursor.execute("select * from TestNumbers order by IntCol"): - print(row) diff --git a/samples/drop_samples.py b/samples/drop_samples.py deleted file mode 100644 index d3d51c7..0000000 --- a/samples/drop_samples.py +++ /dev/null @@ -1,24 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2019, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# drop_samples.py -# -# Drops the database objects used for the cx_Oracle samples. -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb -import sample_env - -def drop_samples(conn): - print("Dropping sample schemas and edition...") - sample_env.run_sql_script(conn, "drop_samples", - main_user=sample_env.get_main_user(), - edition_user=sample_env.get_edition_user(), - edition_name=sample_env.get_edition_name()) - -if __name__ == "__main__": - conn = oracledb.connect(sample_env.get_admin_connect_string()) - drop_samples(conn) - print("Done.") diff --git a/samples/editioning.py b/samples/editioning.py deleted file mode 100644 index 6492c7d..0000000 --- a/samples/editioning.py +++ /dev/null @@ -1,76 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved. -# -# Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. -# -# Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, -# Canada. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# editioning.py -# This script demonstrates the use of Edition-Based Redefinition, available -# in Oracle# Database 11.2 and higher. See the Oracle documentation on the -# subject for additional information. Adjust the contants at the top of the -# script for your own database as needed. -# -# This script requires cx_Oracle 5.3 and higher. -#------------------------------------------------------------------------------ - -import os - -import cx_Oracle as oracledb -import sample_env - -# connect to the editions user and create a procedure -edition_connect_string = sample_env.get_edition_connect_string() -edition_name = sample_env.get_edition_name() -connection = oracledb.connect(edition_connect_string) -print("Edition should be None, actual value is:", repr(connection.edition)) -cursor = connection.cursor() -cursor.execute(""" - create or replace function TestEditions return varchar2 as - begin - return 'Base Procedure'; - end;""") -result = cursor.callfunc("TestEditions", str) -print("Function should return 'Base Procedure', actually returns:", - repr(result)) - -# next, change the edition and recreate the procedure in the new edition -cursor.execute("alter session set edition = %s" % edition_name) -print("Edition should be", repr(edition_name.upper()), - "actual value is:", repr(connection.edition)) -cursor.execute(""" - create or replace function TestEditions return varchar2 as - begin - return 'Edition 1 Procedure'; - end;""") -result = cursor.callfunc("TestEditions", str) -print("Function should return 'Edition 1 Procedure', actually returns:", - repr(result)) - -# next, change the edition back to the base edition and demonstrate that the -# original function is being called -cursor.execute("alter session set edition = ORA$BASE") -result = cursor.callfunc("TestEditions", str) -print("Function should return 'Base Procedure', actually returns:", - repr(result)) - -# the edition can be set upon connection -connection = oracledb.connect(edition_connect_string, - edition=edition_name.upper()) -cursor = connection.cursor() -result = cursor.callfunc("TestEditions", str) -print("Function should return 'Edition 1 Procedure', actually returns:", - repr(result)) - -# it can also be set via the environment variable ORA_EDITION -os.environ["ORA_EDITION"] = edition_name.upper() -connection = oracledb.connect(edition_connect_string) -print("Edition should be", repr(edition_name.upper()), - "actual value is:", repr(connection.edition)) -cursor = connection.cursor() -result = cursor.callfunc("TestEditions", str) -print("Function should return 'Edition 1 Procedure', actually returns:", - repr(result)) diff --git a/samples/generic_row_factory.py b/samples/generic_row_factory.py deleted file mode 100644 index 2608b11..0000000 --- a/samples/generic_row_factory.py +++ /dev/null @@ -1,47 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# generic_row_factory.py -# -# Demonstrate the ability to return named tuples for all queries using a -# subclassed cursor and row factory. -#------------------------------------------------------------------------------ - -import collections - -import cx_Oracle as oracledb -import sample_env - -class Connection(oracledb.Connection): - - def cursor(self): - return Cursor(self) - - -class Cursor(oracledb.Cursor): - - def execute(self, statement, args=None): - prepare_needed = (self.statement != statement) - result = super().execute(statement, args or []) - if prepare_needed: - description = self.description - if description is not None: - names = [d[0] for d in description] - self.rowfactory = collections.namedtuple("GenericQuery", names) - return result - - -# create new subclassed connection and cursor -connection = Connection(sample_env.get_main_connect_string()) -cursor = connection.cursor() - -# the names are now available directly for each query executed -for row in cursor.execute("select ParentId, Description from ParentTable"): - print(row.PARENTID, "->", row.DESCRIPTION) -print() - -for row in cursor.execute("select ChildId, Description from ChildTable"): - print(row.CHILDID, "->", row.DESCRIPTION) -print() diff --git a/samples/implicit_results.py b/samples/implicit_results.py deleted file mode 100644 index 78b7009..0000000 --- a/samples/implicit_results.py +++ /dev/null @@ -1,49 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved. -# -# Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. -# -# Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, -# Canada. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# implicit_results.py -# This script demonstrates the use of the 12.1 feature that allows PL/SQL -# procedures to return result sets implicitly, without having to explicitly -# define them. -# -# This script requires cx_Oracle 5.3 and higher. -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb -import sample_env - -connection = oracledb.connect(sample_env.get_main_connect_string()) -cursor = connection.cursor() - -# use PL/SQL block to return two cursors -cursor.execute(""" - declare - c1 sys_refcursor; - c2 sys_refcursor; - begin - - open c1 for - select * from TestNumbers; - - dbms_sql.return_result(c1); - - open c2 for - select * from TestStrings; - - dbms_sql.return_result(c2); - - end;""") - -# display results -for ix, result_set in enumerate(cursor.getimplicitresults()): - print("Result Set #" + str(ix + 1)) - for row in result_set: - print(row) - print() diff --git a/samples/insert_geometry.py b/samples/insert_geometry.py deleted file mode 100644 index 9df612f..0000000 --- a/samples/insert_geometry.py +++ /dev/null @@ -1,55 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved. -# -# Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. -# -# Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, -# Canada. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# insert_geometry.py -# This script demonstrates the ability to create Oracle objects (this example -# uses SDO_GEOMETRY) and insert them into a table. -# -# This script requires cx_Oracle 5.3 and higher. -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb -import sample_env - -# create and populate Oracle objects -connection = oracledb.connect(sample_env.get_main_connect_string()) -type_obj = connection.gettype("MDSYS.SDO_GEOMETRY") -element_info_type_obj = connection.gettype("MDSYS.SDO_ELEM_INFO_ARRAY") -ordinate_type_obj = connection.gettype("MDSYS.SDO_ORDINATE_ARRAY") -obj = type_obj.newobject() -obj.SDO_GTYPE = 2003 -obj.SDO_ELEM_INFO = element_info_type_obj.newobject() -obj.SDO_ELEM_INFO.extend([1, 1003, 3]) -obj.SDO_ORDINATES = ordinate_type_obj.newobject() -obj.SDO_ORDINATES.extend([1, 1, 5, 7]) -print("Created object", obj) - -# create table, if necessary -cursor = connection.cursor() -cursor.execute(""" - select count(*) - from user_tables - where table_name = 'TESTGEOMETRY'""") -count, = cursor.fetchone() -if count == 0: - print("Creating table...") - cursor.execute(""" - create table TestGeometry ( - IntCol number(9) not null, - Geometry MDSYS.SDO_GEOMETRY not null - )""") - -# remove all existing rows and then add a new one -print("Removing any existing rows...") -cursor.execute("delete from TestGeometry") -print("Adding row to table...") -cursor.execute("insert into TestGeometry values (1, :obj)", obj=obj) -connection.commit() -print("Success!") diff --git a/samples/json_blob.py b/samples/json_blob.py deleted file mode 100644 index 2c3df37..0000000 --- a/samples/json_blob.py +++ /dev/null @@ -1,89 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# json_blob.py -# Shows how to use a BLOB as a JSON column store. -# -# Note: with Oracle Database 21c using the new JSON type is recommended -# instead, see json_direct.py -# -# Documentation: -# cx_Oracle: https://cx-oracle.readthedocs.io/en/latest/user_guide/json_data_type.html -# Oracle Database: https://www.oracle.com/pls/topic/lookup?ctx=dblatest&id=ADJSN -# -#------------------------------------------------------------------------------ - -import json -import sys - -import cx_Oracle as oracledb -import sample_env - -connection = oracledb.connect(sample_env.get_main_connect_string()) - -client_version = oracledb.clientversion()[0] -db_version = int(connection.version.split(".")[0]) - -# Minimum database vesion is 12 -if db_version < 12: - sys.exit("This example requires Oracle Database 12.1.0.2 or later") - -# Create a table - -cursor = connection.cursor() -cursor.execute(""" - begin - execute immediate 'drop table customers'; - exception when others then - if sqlcode <> -942 then - raise; - end if; - end;""") -cursor.execute(""" - create table customers ( - id integer not null primary key, - json_data blob check (json_data is json) - ) lob (json_data) store as (cache)""") - -# Insert JSON data - -data = dict(name="Rod", dept="Sales", location="Germany") -inssql = "insert into customers values (:1, :2)" -if client_version >= 21 and db_version >= 21: - # Take advantage of direct binding - cursor.setinputsizes(None, oracledb.DB_TYPE_JSON) - cursor.execute(inssql, [1, data]) -else: - # Insert the data as a JSON string - cursor.execute(inssql, [1, json.dumps(data)]) - -# Select JSON data - -sql = "SELECT c.json_data FROM customers c" -for j, in cursor.execute(sql): - print(json.loads(j.read())) - -# Using JSON_VALUE to extract a value from a JSON column - -sql = """SELECT JSON_VALUE(json_data, '$.location') - FROM customers - OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY""" -for r in cursor.execute(sql): - print(r) - -# Using dot-notation to extract a value from a JSON (BLOB storage) column - -sql = """SELECT c.json_data.location - FROM customers c - OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY""" -for j, in cursor.execute(sql): - print(j) - -# Using JSON_OBJECT to extract relational data as JSON - -sql = """SELECT JSON_OBJECT('key' IS d.dummy) dummy - FROM dual d""" -for r in cursor.execute(sql): - print(r) diff --git a/samples/json_direct.py b/samples/json_direct.py deleted file mode 100644 index a5bcad9..0000000 --- a/samples/json_direct.py +++ /dev/null @@ -1,94 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# json_direct.py -# Shows some JSON features of Oracle Database 21c. -# See https://www.oracle.com/pls/topic/lookup?ctx=dblatest&id=ADJSN -# -# For JSON with older databases see json_blob.py -#------------------------------------------------------------------------------ - -import json -import sys - -import cx_Oracle as oracledb -import sample_env - -connection = oracledb.connect(sample_env.get_main_connect_string()) - -client_version = oracledb.clientversion()[0] -db_version = int(connection.version.split(".")[0]) - -# this script only works with Oracle Database 21 - -if db_version < 21: - sys.exit("This example requires Oracle Database 21.1 or later. " - "Try json_blob.py") - -# Create a table - -cursor = connection.cursor() -cursor.execute(""" - begin - execute immediate 'drop table customers'; - exception when others then - if sqlcode <> -942 then - raise; - end if; - end;""") -cursor.execute(""" - create table customers ( - id integer not null primary key, - json_data json - )""") - -# Insert JSON data - -data = dict(name="Rod", dept="Sales", location="Germany") -inssql = "insert into customers values (:1, :2)" -if client_version >= 21: - # Take advantage of direct binding - cursor.setinputsizes(None, oracledb.DB_TYPE_JSON) - cursor.execute(inssql, [1, data]) -else: - # Insert the data as a JSON string - cursor.execute(inssql, [1, json.dumps(data)]) - -# Select JSON data - -sql = "SELECT c.json_data FROM customers c" -if client_version >= 21: - for j, in cursor.execute(sql): - print(j) -else: - for j, in cursor.execute(sql): - print(json.loads(j.read())) - -# Using JSON_VALUE to extract a value from a JSON column - -sql = """SELECT JSON_VALUE(json_data, '$.location') - FROM customers - OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY""" -for r in cursor.execute(sql): - print(r) - -# Using dot-notation to extract a value from a JSON column - -sql = """SELECT c.json_data.location - FROM customers c - OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY""" -if client_version >= 21: - for j, in cursor.execute(sql): - print(j) -else: - for j, in cursor.execute(sql): - print(json.loads(j.read())) - -# Using JSON_OBJECT to extract relational data as JSON - -sql = """SELECT JSON_OBJECT('key' IS d.dummy) dummy - FROM dual d""" -for r in cursor.execute(sql): - print(r) diff --git a/samples/last_rowid.py b/samples/last_rowid.py deleted file mode 100644 index 948166b..0000000 --- a/samples/last_rowid.py +++ /dev/null @@ -1,56 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2019, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# last_rowid.py -# Demonstrates the use of the cursor.lastrowid attribute. -# -# This script requires cx_Oracle 7.3 and higher. -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb -import sample_env - -connection = oracledb.connect(sample_env.get_main_connect_string()) - -row1 = [1, "First"] -row2 = [2, "Second"] - -# insert a couple of rows and retain the rowid of each -cursor = connection.cursor() -cursor.execute("insert into mytab (id, data) values (:1, :2)", row1) -rowid1 = cursor.lastrowid -print("Row 1:", row1) -print("Rowid 1:", rowid1) -print() - -cursor.execute("insert into mytab (id, data) values (:1, :2)", row2) -rowid2 = cursor.lastrowid -print("Row 2:", row2) -print("Rowid 2:", rowid2) -print() - -# the row can be fetched with the rowid that was retained -cursor.execute("select id, data from mytab where rowid = :1", [rowid1]) -print("Row 1:", cursor.fetchone()) -cursor.execute("select id, data from mytab where rowid = :1", [rowid2]) -print("Row 2:", cursor.fetchone()) -print() - -# updating multiple rows only returns the rowid of the last updated row -cursor.execute("update mytab set data = data || ' (Modified)'") -cursor.execute("select id, data from mytab where rowid = :1", - [cursor.lastrowid]) -print("Last updated row:", cursor.fetchone()) - -# deleting multiple rows only returns the rowid of the last deleted row -cursor.execute("delete from mytab") -print("Rowid of last deleted row:", cursor.lastrowid) - -# deleting no rows results in a value of None -cursor.execute("delete from mytab") -print("Rowid when no rows are deleted:", cursor.lastrowid) - -# Don't commit - this lets us run the demo multiple times -#connection.commit() diff --git a/samples/multi_consumer_aq.py b/samples/multi_consumer_aq.py deleted file mode 100644 index a03d66e..0000000 --- a/samples/multi_consumer_aq.py +++ /dev/null @@ -1,67 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. -# -# Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. -# -# Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, -# Canada. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# multi_consumer_aq.py -# This script demonstrates how to use multi-consumer advanced queuing. It -# makes use of a RAW queue created in the sample setup. -# -# This script requires cx_Oracle 8.2 and higher. -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb -import sample_env - -QUEUE_NAME = "DEMO_RAW_QUEUE_MULTI" -PAYLOAD_DATA = [ - "The first message", - "The second message", - "The third message", - "The fourth and final message" -] - -# connect to database -connection = oracledb.connect(sample_env.get_main_connect_string()) -cursor = connection.cursor() - -# create queue -queue = connection.queue(QUEUE_NAME) -queue.deqoptions.wait = oracledb.DEQ_NO_WAIT -queue.deqoptions.navigation = oracledb.DEQ_FIRST_MSG - -# enqueue a few messages -print("Enqueuing messages...") -for data in PAYLOAD_DATA: - print(data) - queue.enqone(connection.msgproperties(payload=data)) -connection.commit() -print() - -# dequeue the messages for consumer A -print("Dequeuing the messages for consumer A...") -queue.deqoptions.consumername = "SUBSCRIBER_A" -while True: - props = queue.deqone() - if not props: - break - print(props.payload.decode()) -connection.commit() -print() - -# dequeue the message for consumer B -print("Dequeuing the messages for consumer B...") -queue.deqoptions.consumername = "SUBSCRIBER_B" -while True: - props = queue.deqone() - if not props: - break - print(props.payload.decode()) -connection.commit() - -print("\nDone.") diff --git a/samples/object_aq.py b/samples/object_aq.py deleted file mode 100644 index 268e5a2..0000000 --- a/samples/object_aq.py +++ /dev/null @@ -1,66 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved. -# -# Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. -# -# Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, -# Canada. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# object_aq.py -# This script demonstrates how to use advanced queuing with objects. It makes -# use of a simple type and queue created in the sample setup. -# -# This script requires cx_Oracle 8.2 and higher. -#------------------------------------------------------------------------------ - -import decimal - -import cx_Oracle as oracledb -import sample_env - -BOOK_TYPE_NAME = "UDT_BOOK" -QUEUE_NAME = "DEMO_BOOK_QUEUE" -BOOK_DATA = [ - ("The Fellowship of the Ring", "Tolkien, J.R.R.", - decimal.Decimal("10.99")), - ("Harry Potter and the Philosopher's Stone", "Rowling, J.K.", - decimal.Decimal("7.99")) -] - -# connect to database -connection = oracledb.connect(sample_env.get_main_connect_string()) -cursor = connection.cursor() - -# create queue -books_type = connection.gettype(BOOK_TYPE_NAME) -queue = connection.queue(QUEUE_NAME, payload_type=books_type) -queue.deqoptions.wait = oracledb.DEQ_NO_WAIT -queue.deqoptions.navigation = oracledb.DEQ_FIRST_MSG - -# dequeue all existing messages to ensure the queue is empty, just so that -# the results are consistent -while queue.deqone(): - pass - -# enqueue a few messages -print("Enqueuing messages...") -for title, authors, price in BOOK_DATA: - book = books_type.newobject() - book.TITLE = title - book.AUTHORS = authors - book.PRICE = price - print(title) - queue.enqone(connection.msgproperties(payload=book)) -connection.commit() - -# dequeue the messages -print("\nDequeuing messages...") -while True: - props = queue.deqone() - if not props: - break - print(props.payload.TITLE) -connection.commit() -print("\nDone.") diff --git a/samples/plsql_collection.py b/samples/plsql_collection.py deleted file mode 100644 index 4699720..0000000 --- a/samples/plsql_collection.py +++ /dev/null @@ -1,46 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# plsql_collection.py -# -# Demonstrate how to get the value of a PL/SQL collection from a stored -# procedure. -# -# This feature is new in cx_Oracle 5.3 and is only available in Oracle -# Database 12.1 and higher. The ability to get the collection as a dictionary -# is new in cx_Oracle 7.0. -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb -import sample_env - -connection = oracledb.connect(sample_env.get_main_connect_string()) - -# create new empty object of the correct type -# note the use of a PL/SQL type defined in a package -type_obj = connection.gettype("PKG_DEMO.UDT_STRINGLIST") -obj = type_obj.newobject() - -# call the stored procedure which will populate the object -cursor = connection.cursor() -cursor.callproc("pkg_Demo.DemoCollectionOut", (obj,)) - -# show the indexes that are used by the collection -print("Indexes and values of collection:") -ix = obj.first() -while ix is not None: - print(ix, "->", obj.getelement(ix)) - ix = obj.next(ix) -print() - -# show the values as a simple list -print("Values of collection as list:") -print(obj.aslist()) -print() - -# show the values as a simple dictionary -print("Values of collection as dictionary:") -print(obj.asdict()) -print() diff --git a/samples/plsql_function.py b/samples/plsql_function.py deleted file mode 100644 index b67cf4f..0000000 --- a/samples/plsql_function.py +++ /dev/null @@ -1,18 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# plsql_function.py -# -# Demonstrate how to call a PL/SQL function and get its return value. -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb -import sample_env - -connection = oracledb.connect(sample_env.get_main_connect_string()) - -cursor = connection.cursor() -res = cursor.callfunc('myfunc', int, ('abc', 2)) -print(res) diff --git a/samples/plsql_procedure.py b/samples/plsql_procedure.py deleted file mode 100644 index 113d880..0000000 --- a/samples/plsql_procedure.py +++ /dev/null @@ -1,20 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# plsql_procedure.py -# -# Demonstrate how to call a PL/SQL stored procedure and get the results of an -# OUT variable. -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb -import sample_env - -connection = oracledb.connect(sample_env.get_main_connect_string()) - -cursor = connection.cursor() -myvar = cursor.var(int) -cursor.callproc('myproc', (123, myvar)) -print(myvar.getvalue()) diff --git a/samples/plsql_record.py b/samples/plsql_record.py deleted file mode 100644 index 81e3ebc..0000000 --- a/samples/plsql_record.py +++ /dev/null @@ -1,46 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# plsql_record.py -# -# Demonstrate how to bind (in and out) a PL/SQL record. -# -# This feature is only available in Oracle Database 12.1 and higher. -#------------------------------------------------------------------------------ - -import datetime - -import cx_Oracle as oracledb -import sample_env - -connection = oracledb.connect(sample_env.get_main_connect_string()) - -# create new object of the correct type -# note the use of a PL/SQL record defined in a package -# a table record identified by TABLE%ROWTYPE can also be used -type_obj = connection.gettype("PKG_DEMO.UDT_DEMORECORD") -obj = type_obj.newobject() -obj.NUMBERVALUE = 6 -obj.STRINGVALUE = "Test String" -obj.DATEVALUE = datetime.datetime(2016, 5, 28) -obj.BOOLEANVALUE = False - -# show the original values -print("NUMBERVALUE ->", obj.NUMBERVALUE) -print("STRINGVALUE ->", obj.STRINGVALUE) -print("DATEVALUE ->", obj.DATEVALUE) -print("BOOLEANVALUE ->", obj.BOOLEANVALUE) -print() - -# call the stored procedure which will modify the object -cursor = connection.cursor() -cursor.callproc("pkg_Demo.DemoRecordsInOut", (obj,)) - -# show the modified values -print("NUMBERVALUE ->", obj.NUMBERVALUE) -print("STRINGVALUE ->", obj.STRINGVALUE) -print("DATEVALUE ->", obj.DATEVALUE) -print("BOOLEANVALUE ->", obj.BOOLEANVALUE) -print() diff --git a/samples/query.py b/samples/query.py deleted file mode 100644 index d96299c..0000000 --- a/samples/query.py +++ /dev/null @@ -1,46 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# query.py -# -# Demonstrate how to perform a query in different ways. -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb -import sample_env - -connection = oracledb.connect(sample_env.get_main_connect_string()) - -sql = """ - select * from SampleQueryTab - where id < 6 - order by id""" - -print("Get all rows via iterator") -cursor = connection.cursor() -for result in cursor.execute(sql): - print(result) -print() - -print("Query one row at a time") -cursor.execute(sql) -row = cursor.fetchone() -print(row) -row = cursor.fetchone() -print(row) -print() - -print("Fetch many rows") -cursor.execute(sql) -res = cursor.fetchmany(3) -print(res) -print() - -print("Fetch each row as a Dictionary") -cursor.execute(sql) -columns = [col[0] for col in cursor.description] -cursor.rowfactory = lambda *args: dict(zip(columns, args)) -for row in cursor: - print(row) diff --git a/samples/query_arraysize.py b/samples/query_arraysize.py deleted file mode 100644 index 661ddbf..0000000 --- a/samples/query_arraysize.py +++ /dev/null @@ -1,30 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# query_arraysize.py -# -# Demonstrate how to alter the array size and prefetch rows value on a cursor -# in order to reduce the number of network round trips and overhead required to -# fetch all of the rows from a large table. -#------------------------------------------------------------------------------ - -import time - -import cx_Oracle as oracledb -import sample_env - -connection = oracledb.connect(sample_env.get_main_connect_string()) - -start = time.time() - -cursor = connection.cursor() -cursor.prefetchrows = 1000 -cursor.arraysize = 1000 -cursor.execute('select * from bigtab') -res = cursor.fetchall() -# print(res) # uncomment to display the query results - -elapsed = (time.time() - start) -print("Retrieved", len(res), "rows in", elapsed, "seconds") diff --git a/samples/query_strings_as_bytes.py b/samples/query_strings_as_bytes.py deleted file mode 100644 index 51b7e43..0000000 --- a/samples/query_strings_as_bytes.py +++ /dev/null @@ -1,49 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# query_strings_as_bytes.py -# -# Demonstrates how to query strings as bytes (bypassing decoding of the bytes -# into a Python string). This can be useful when attempting to fetch data that -# was stored in the database in the wrong encoding. -# -# This script requires cx_Oracle 8.2 and higher. -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb -import sample_env - -STRING_VAL = 'I bought a cafetière on the Champs-Élysées' - -def return_strings_as_bytes(cursor, name, default_type, size, precision, - scale): - if default_type == oracledb.DB_TYPE_VARCHAR: - return cursor.var(str, arraysize=cursor.arraysize, bypass_decode=True) - -with oracledb.connect(sample_env.get_main_connect_string()) as conn: - - # truncate table and populate with our data of choice - with conn.cursor() as cursor: - cursor.execute("truncate table TestTempTable") - cursor.execute("insert into TestTempTable values (1, :val)", - val=STRING_VAL) - conn.commit() - - # fetch the data normally and show that it is returned as a string - with conn.cursor() as cursor: - cursor.execute("select IntCol, StringCol from TestTempTable") - print("Data fetched using normal technique:") - for row in cursor: - print(row) - print() - - # fetch the data, bypassing the decode and show that it is returned as - # bytes - with conn.cursor() as cursor: - cursor.outputtypehandler = return_strings_as_bytes - cursor.execute("select IntCol, StringCol from TestTempTable") - print("Data fetched using bypass decode technique:") - for row in cursor: - print(row) diff --git a/samples/raw_aq.py b/samples/raw_aq.py deleted file mode 100644 index f070ed9..0000000 --- a/samples/raw_aq.py +++ /dev/null @@ -1,58 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2019, 2021, Oracle and/or its affiliates. All rights reserved. -# -# Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. -# -# Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, -# Canada. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# raw_aq.py -# This script demonstrates how to use advanced queuing with RAW data. It -# makes use of a RAW queue created in the sample setup. -# -# This script requires cx_Oracle 8.2 and higher. -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb -import sample_env - -QUEUE_NAME = "DEMO_RAW_QUEUE" -PAYLOAD_DATA = [ - "The first message", - "The second message", - "The third message", - "The fourth and final message" -] - -# connect to database -connection = oracledb.connect(sample_env.get_main_connect_string()) -cursor = connection.cursor() - -# create queue -queue = connection.queue(QUEUE_NAME) -queue.deqoptions.wait = oracledb.DEQ_NO_WAIT -queue.deqoptions.navigation = oracledb.DEQ_FIRST_MSG - -# dequeue all existing messages to ensure the queue is empty, just so that -# the results are consistent -while queue.deqone(): - pass - -# enqueue a few messages -print("Enqueuing messages...") -for data in PAYLOAD_DATA: - print(data) - queue.enqone(connection.msgproperties(payload=data)) -connection.commit() - -# dequeue the messages -print("\nDequeuing messages...") -while True: - props = queue.deqone() - if not props: - break - print(props.payload.decode()) -connection.commit() -print("\nDone.") diff --git a/samples/ref_cursor.py b/samples/ref_cursor.py deleted file mode 100644 index eed4eff..0000000 --- a/samples/ref_cursor.py +++ /dev/null @@ -1,55 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2018, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# ref_cursor.py -# Demonstrates the use of REF cursors. -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb -import sample_env - -connection = oracledb.connect(sample_env.get_main_connect_string()) -cursor = connection.cursor() - -ref_cursor = connection.cursor() -cursor.callproc("myrefcursorproc", (2, 6, ref_cursor)) -print("Rows between 2 and 6:") -for row in ref_cursor: - print(row) -print() - -ref_cursor = connection.cursor() -cursor.callproc("myrefcursorproc", (8, 9, ref_cursor)) -print("Rows between 8 and 9:") -for row in ref_cursor: - print(row) -print() - -#------------------------------------------------------------------------------ -# Setting prefetchrows and arraysize of a REF cursor can improve performance -# when fetching a large number of rows (Tuned Fetch) -#------------------------------------------------------------------------------ - -# Truncate the table used for this demo -cursor.execute("truncate table TestTempTable") - -# Populate the table with a large number of rows -num_rows = 50000 -sql = "insert into TestTempTable (IntCol) values (:1)" -data = [(n + 1,) for n in range(num_rows)] -cursor.executemany(sql, data) - -# Set the arraysize and prefetch rows of the REF cursor -ref_cursor = connection.cursor() -ref_cursor.prefetchrows = 1000 -ref_cursor.arraysize = 1000 - -# Perform the tuned fetch -sum_rows = 0 -cursor.callproc("myrefcursorproc2", [ref_cursor]) -print("Sum of IntCol for", num_rows, "rows:") -for row in ref_cursor: - sum_rows += row[0] -print(sum_rows) diff --git a/samples/return_lobs_as_strings.py b/samples/return_lobs_as_strings.py deleted file mode 100644 index 1c42069..0000000 --- a/samples/return_lobs_as_strings.py +++ /dev/null @@ -1,71 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved. -# -# Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. -# -# Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, -# Canada. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# return_lobs_as_strings.py -# Returns all CLOB values as strings and BLOB values as bytes. The -# performance of this technique is significantly better than fetching the LOBs -# and then reading the contents of the LOBs as it avoids round-trips to the -# database. Be aware, however, that this method requires contiguous memory so -# is not usable for very large LOBs. -# -# This script requires cx_Oracle 5.0 and higher. -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb -import sample_env - -def output_type_handler(cursor, name, default_type, size, precision, scale): - if default_type == oracledb.CLOB: - return cursor.var(oracledb.LONG_STRING, arraysize=cursor.arraysize) - if default_type == oracledb.BLOB: - return cursor.var(oracledb.LONG_BINARY, arraysize=cursor.arraysize) - -connection = oracledb.connect(sample_env.get_main_connect_string()) -connection.outputtypehandler = output_type_handler -cursor = connection.cursor() - -# add some data to the tables -print("Populating tables with data...") -cursor.execute("truncate table TestClobs") -cursor.execute("truncate table TestBlobs") -long_string = "" -for i in range(10): - char = chr(ord('A') + i) - long_string += char * 25000 - # uncomment the line below for cx_Oracle 5.3 and earlier - # cursor.setinputsizes(None, oracledb.LONG_STRING) - cursor.execute("insert into TestClobs values (:1, :2)", - (i + 1, "STRING " + long_string)) - # uncomment the line below for cx_Oracle 5.3 and earlier - # cursor.setinputsizes(None, oracledb.LONG_BINARY) - cursor.execute("insert into TestBlobs values (:1, :2)", - (i + 1, long_string.encode("ascii"))) -connection.commit() - -# fetch the data and show the results -print("CLOBS returned as strings") -cursor.execute(""" - select - IntCol, - ClobCol - from TestClobs - order by IntCol""") -for int_col, value in cursor: - print("Row:", int_col, "string of length", len(value)) -print() -print("BLOBS returned as bytes") -cursor.execute(""" - select - IntCol, - BlobCol - from TestBlobs - order by IntCol""") -for int_col, value in cursor: - print("Row:", int_col, "string of length", value and len(value) or 0) diff --git a/samples/return_numbers_as_decimals.py b/samples/return_numbers_as_decimals.py deleted file mode 100644 index becb972..0000000 --- a/samples/return_numbers_as_decimals.py +++ /dev/null @@ -1,31 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# return_numbers_as_decimals.py -# Returns all numbers as decimals by means of an output type handler. This is -# needed if the full decimal precision of Oracle numbers is required by the -# application. See this article -# (http://blog.reverberate.org/2016/02/06/floating-point-demystified-part2.html) -# for an explanation of why decimal numbers (like Oracle numbers) cannot be -# represented exactly by floating point numbers. -# -# This script requires cx_Oracle 5.0 and higher. -#------------------------------------------------------------------------------ - -import decimal - -import cx_Oracle as oracledb -import sample_env - -def output_type_handler(cursor, name, default_type, size, precision, scale): - if default_type == oracledb.NUMBER: - return cursor.var(decimal.Decimal, arraysize=cursor.arraysize) - -connection = oracledb.connect(sample_env.get_main_connect_string()) -connection.outputtypehandler = output_type_handler -cursor = connection.cursor() -cursor.execute("select * from TestNumbers") -for row in cursor: - print("Row:", row) diff --git a/samples/rows_as_instance.py b/samples/rows_as_instance.py deleted file mode 100644 index 1ab1c63..0000000 --- a/samples/rows_as_instance.py +++ /dev/null @@ -1,57 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved. -# -# Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. -# -# Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, -# Canada. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# rows_as_instance.py -# Returns rows as instances instead of tuples. See the ceDatabase.Row class -# in the cx_PyGenLib project (http://cx-pygenlib.sourceforge.net) for a more -# advanced example. -# -# This script requires cx_Oracle 4.3 and higher. -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb -import sample_env - -class Test: - - def __init__(self, a, b, c): - self.a = a - self.b = b - self.c = c - -connection = oracledb.connect(sample_env.get_main_connect_string()) -cursor = connection.cursor() - -# change this to False if you want to create the table yourself using SQL*Plus -# and then populate it with the data of your choice -if True: - cursor.execute(""" - select count(*) - from user_tables - where table_name = 'TESTINSTANCES'""") - count, = cursor.fetchone() - if count: - cursor.execute("drop table TestInstances") - cursor.execute(""" - create table TestInstances ( - a varchar2(60) not null, - b number(9) not null, - c date not null - )""") - cursor.execute("insert into TestInstances values ('First', 5, sysdate)") - cursor.execute("insert into TestInstances values ('Second', 25, sysdate)") - connection.commit() - -# retrieve the data and display it -cursor.execute("select * from TestInstances") -cursor.rowfactory = Test -print("Rows:") -for row in cursor: - print("a = %s, b = %s, c = %s" % (row.a, row.b, row.c)) diff --git a/samples/sample_env.py b/samples/sample_env.py deleted file mode 100644 index 4d87451..0000000 --- a/samples/sample_env.py +++ /dev/null @@ -1,164 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2017, 2020, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# Sets the environment used by the sample scripts. Production applications -# should consider using External Authentication to avoid hard coded -# credentials. -# -# You can set values in environment variables to bypass the sample requesting -# the information it requires. -# -# CX_ORACLE_SAMPLES_MAIN_USER: user used for most samples -# CX_ORACLE_SAMPLES_MAIN_PASSWORD: password of user used for most samples -# CX_ORACLE_SAMPLES_EDITION_USER: user for editioning -# CX_ORACLE_SAMPLES_EDITION_PASSWORD: password of user for editioning -# CX_ORACLE_SAMPLES_EDITION_NAME: name of edition for editioning -# CX_ORACLE_SAMPLES_CONNECT_STRING: connect string -# CX_ORACLE_SAMPLES_DRCP_CONNECT_STRING: DRCP connect string -# CX_ORACLE_SAMPLES_ADMIN_USER: admin user for setting up samples -# CX_ORACLE_SAMPLES_ADMIN_PASSWORD: admin password for setting up samples -# -# CX_ORACLE_SAMPLES_CONNECT_STRING can be set to an Easy Connect string, or a -# Net Service Name from a tnsnames.ora file or external naming service, -# or it can be the name of a local Oracle database instance. -# -# If using Instant Client, then an Easy Connect string is generally -# appropriate. The syntax is: -# -# [//]host_name[:port][/service_name][:server_type][/instance_name] -# -# Commonly just the host_name and service_name are needed -# e.g. "localhost/orclpdb1" or "localhost/XEPDB1" -# -# If using a tnsnames.ora file, the file can be in a default -# location such as $ORACLE_HOME/network/admin/tnsnames.ora or -# /etc/tnsnames.ora. Alternatively set the TNS_ADMIN environment -# variable and put the file in $TNS_ADMIN/tnsnames.ora. -# -# The administrative user for cloud databases is ADMIN and the administrative -# user for on premises databases is SYSTEM. -#------------------------------------------------------------------------------ - -print(""" - -!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!!! cx_Oracle was renamed to python-oracledb. !!! -!!! Use python-oracledb instead of cx_Oracle for development. !!! -!!! Install with: !!! -!!! python -m pip install oracledb !!! -!!! and use the new samples in: !!! -!!! https://github.com/oracle/python-oracledb/tree/main/samples !!! -!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - -""") - -import getpass -import os -import sys - -# default values -DEFAULT_MAIN_USER = "pythondemo" -DEFAULT_EDITION_USER = "pythoneditions" -DEFAULT_EDITION_NAME = "python_e1" -DEFAULT_CONNECT_STRING = "localhost/orclpdb1" -DEFAULT_DRCP_CONNECT_STRING = "localhost/orclpdb1:pooled" - -# dictionary containing all parameters; these are acquired as needed by the -# methods below (which should be used instead of consulting this dictionary -# directly) and then stored so that a value is not requested more than once -PARAMETERS = {} - -def get_value(name, label, default_value=""): - value = PARAMETERS.get(name) - if value is not None: - return value - env_name = "CX_ORACLE_SAMPLES_" + name - value = os.environ.get(env_name) - if value is None: - if default_value: - label += " [%s]" % default_value - label += ": " - if default_value: - value = input(label).strip() - else: - value = getpass.getpass(label) - if not value: - value = default_value - PARAMETERS[name] = value - return value - -def get_main_user(): - return get_value("MAIN_USER", "Main User Name", DEFAULT_MAIN_USER) - -def get_main_password(): - return get_value("MAIN_PASSWORD", "Password for %s" % get_main_user()) - -def get_edition_user(): - return get_value("EDITION_USER", "Edition User Name", DEFAULT_EDITION_USER) - -def get_edition_password(): - return get_value("EDITION_PASSWORD", - "Password for %s" % get_edition_user()) - -def get_edition_name(): - return get_value("EDITION_NAME", "Edition Name", DEFAULT_EDITION_NAME) - -def get_connect_string(): - return get_value("CONNECT_STRING", "Connect String", - DEFAULT_CONNECT_STRING) - -def get_main_connect_string(password=None): - if password is None: - password = get_main_password() - return "%s/%s@%s" % (get_main_user(), password, get_connect_string()) - -def get_drcp_connect_string(): - connect_string = get_value("DRCP_CONNECT_STRING", "DRCP Connect String", - DEFAULT_DRCP_CONNECT_STRING) - return "%s/%s@%s" % (get_main_user(), get_main_password(), connect_string) - -def get_edition_connect_string(): - return "%s/%s@%s" % \ - (get_edition_user(), get_edition_password(), get_connect_string()) - -def get_admin_connect_string(): - admin_user = get_value("ADMIN_USER", "Administrative user", "admin") - admin_password = get_value("ADMIN_PASSWORD", "Password for %s" % admin_user) - return "%s/%s@%s" % (admin_user, admin_password, get_connect_string()) - -def run_sql_script(conn, script_name, **kwargs): - statement_parts = [] - cursor = conn.cursor() - replace_values = [("&" + k + ".", v) for k, v in kwargs.items()] + \ - [("&" + k, v) for k, v in kwargs.items()] - script_dir = os.path.dirname(os.path.abspath(sys.argv[0])) - file_name = os.path.join(script_dir, "sql", script_name + "_exec.sql") - for line in open(file_name): - if line.strip() == "/": - statement = "".join(statement_parts).strip() - if statement: - for search_value, replace_value in replace_values: - statement = statement.replace(search_value, replace_value) - try: - cursor.execute(statement) - except: - print("Failed to execute SQL:", statement) - raise - statement_parts = [] - else: - statement_parts.append(line) - cursor.execute(""" - select name, type, line, position, text - from dba_errors - where owner = upper(:owner) - order by name, type, line, position""", - owner = get_main_user()) - prev_name = prev_obj_type = None - for name, obj_type, line_num, position, text in cursor: - if name != prev_name or obj_type != prev_obj_type: - print("%s (%s)" % (name, obj_type)) - prev_name = name - prev_obj_type = obj_type - print(" %s/%s %s" % (line_num, position, text)) diff --git a/samples/scrollable_cursors.py b/samples/scrollable_cursors.py deleted file mode 100644 index 3fb855b..0000000 --- a/samples/scrollable_cursors.py +++ /dev/null @@ -1,70 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved. -# -# Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. -# -# Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, -# Canada. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# scrollable_cursors.py -# This script demonstrates how to use scrollable cursors. These allow moving -# forward and backward in the result set but incur additional overhead on the -# server to retain this information. -# -# This script requires cx_Oracle 5.3 and higher. -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb -import sample_env - -connection = oracledb.connect(sample_env.get_main_connect_string()) - -# show all of the rows available in the table -cursor = connection.cursor() -cursor.execute("select * from TestStrings order by IntCol") -print("ALL ROWS") -for row in cursor: - print(row) -print() - -# create a scrollable cursor -cursor = connection.cursor(scrollable = True) - -# set array size smaller than the default (100) to force scrolling by the -# database; otherwise, scrolling occurs directly within the buffers -cursor.arraysize = 3 -cursor.execute("select * from TestStrings order by IntCol") - -# scroll to last row in the result set; the first parameter is not needed and -# is ignored) -cursor.scroll(mode = "last") -print("LAST ROW") -print(cursor.fetchone()) -print() - -# scroll to the first row in the result set; the first parameter not needed and -# is ignored -cursor.scroll(mode = "first") -print("FIRST ROW") -print(cursor.fetchone()) -print() - -# scroll to an absolute row number -cursor.scroll(5, mode = "absolute") -print("ROW 5") -print(cursor.fetchone()) -print() - -# scroll forward six rows (the mode parameter defaults to relative) -cursor.scroll(3) -print("SKIP 3 ROWS") -print(cursor.fetchone()) -print() - -# scroll backward four rows (the mode parameter defaults to relative) -cursor.scroll(-4) -print("SKIP BACK 4 ROWS") -print(cursor.fetchone()) -print() diff --git a/samples/session_callback.py b/samples/session_callback.py deleted file mode 100644 index a1507fc..0000000 --- a/samples/session_callback.py +++ /dev/null @@ -1,138 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2019, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# session_callback.py -# -# Demonstrate how to use a connection pool session callback written in -# Python. The callback is invoked whenever a newly created session is acquired -# from the pool, or when the requested tag does not match the tag that is -# associated with the session. It is generally used to set session state, so -# that the application can count on known session state, which allows the -# application to reduce the number of round trips made to the database. -# If all your connections should have the same session state, you can simplify -# the session callback by removing the tagging logic. -# -# This script requires cx_Oracle 7.1 or higher. -# -# Also see session_callback_plsql.py -# -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb -import sample_env - -# define a dictionary of NLS_DATE_FORMAT formats supported by this sample -SUPPORTED_FORMATS = { - "SIMPLE" : "'YYYY-MM-DD HH24:MI'", - "FULL" : "'YYYY-MM-DD HH24:MI:SS'" -} - -# define a dictionary of TIME_ZONE values supported by this sample -SUPPORTED_TIME_ZONES = { - "UTC" : "'UTC'", - "MST" : "'-07:00'" -} - -# define a dictionary of keys that are supported by this sample -SUPPORTED_KEYS = { - "NLS_DATE_FORMAT" : SUPPORTED_FORMATS, - "TIME_ZONE" : SUPPORTED_TIME_ZONES -} - -# define session callback -def init_session(conn, requested_tag): - - # display the requested and actual tags - print("init_session(): requested tag=%r, actual tag=%r" % \ - (requested_tag, conn.tag)) - - # tags are expected to be in the form "key1=value1;key2=value2" - # in this example, they are used to set NLS parameters and the tag is - # parsed to validate it - if requested_tag is not None: - state_parts = [] - for directive in requested_tag.split(";"): - parts = directive.split("=") - if len(parts) != 2: - raise ValueError("Tag must contain key=value pairs") - key, value = parts - value_dict = SUPPORTED_KEYS.get(key) - if value_dict is None: - raise ValueError("Tag only supports keys: %s" % \ - (", ".join(SUPPORTED_KEYS))) - actual_value = value_dict.get(value) - if actual_value is None: - raise ValueError("Key %s only supports values: %s" % \ - (key, ", ".join(value_dict))) - state_parts.append("%s = %s" % (key, actual_value)) - sql = "alter session set %s" % " ".join(state_parts) - cursor = conn.cursor() - cursor.execute(sql) - - # assign the requested tag to the connection so that when the connection - # is closed, it will automatically be retagged; note that if the requested - # tag is None (no tag was requested) this has no effect - conn.tag = requested_tag - - -# create pool with session callback defined -pool = oracledb.SessionPool(user=sample_env.get_main_user(), - password=sample_env.get_main_password(), - dsn=sample_env.get_connect_string(), min=2, max=5, - increment=1, session_callback=init_session) - -# acquire session without specifying a tag; since the session returned is -# newly created, the callback will be invoked but since there is no tag -# specified, no session state will be changed -print("(1) acquire session without tag") -with pool.acquire() as conn: - cursor = conn.cursor() - cursor.execute("select to_char(current_date) from dual") - result, = cursor.fetchone() - print("main(): result is", repr(result)) - -# acquire session, specifying a tag; since the session returned has no tag, -# the callback will be invoked; session state will be changed and the tag will -# be saved when the connection is closed -print("(2) acquire session with tag") -with pool.acquire(tag="NLS_DATE_FORMAT=SIMPLE") as conn: - cursor = conn.cursor() - cursor.execute("select to_char(current_date) from dual") - result, = cursor.fetchone() - print("main(): result is", repr(result)) - -# acquire session, specifying the same tag; since a session exists in the pool -# with this tag, it will be returned and the callback will not be invoked but -# the connection will still have the session state defined previously -print("(3) acquire session with same tag") -with pool.acquire(tag="NLS_DATE_FORMAT=SIMPLE") as conn: - cursor = conn.cursor() - cursor.execute("select to_char(current_date) from dual") - result, = cursor.fetchone() - print("main(): result is", repr(result)) - -# acquire session, specifying a different tag; since no session exists in the -# pool with this tag, a new session will be returned and the callback will be -# invoked; session state will be changed and the tag will be saved when the -# connection is closed -print("(4) acquire session with different tag") -with pool.acquire(tag="NLS_DATE_FORMAT=FULL;TIME_ZONE=UTC") as conn: - cursor = conn.cursor() - cursor.execute("select to_char(current_date) from dual") - result, = cursor.fetchone() - print("main(): result is", repr(result)) - -# acquire session, specifying a different tag but also specifying that a -# session with any tag can be acquired from the pool; a session with one of the -# previously set tags will be returned and the callback will be invoked; -# session state will be changed and the tag will be saved when the connection -# is closed -print("(4) acquire session with different tag but match any also specified") -with pool.acquire(tag="NLS_DATE_FORMAT=FULL;TIME_ZONE=MST", matchanytag=True) \ - as conn: - cursor = conn.cursor() - cursor.execute("select to_char(current_date) from dual") - result, = cursor.fetchone() - print("main(): result is", repr(result)) diff --git a/samples/session_callback_plsql.py b/samples/session_callback_plsql.py deleted file mode 100644 index a25486f..0000000 --- a/samples/session_callback_plsql.py +++ /dev/null @@ -1,102 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2019, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# session_callback_plsql.py -# -# Demonstrate how to use a connection pool session callback written in -# PL/SQL. The callback is invoked whenever the tag requested by the application -# does not match the tag associated with the session in the pool. It should be -# used to set session state, so that the application can count on known session -# state, which allows the application to reduce the number of round trips to the -# database. -# -# The primary advantage to this approach over the equivalent approach shown in -# session_callback.py is when DRCP is used, as the callback is invoked on the -# server and no round trip is required to set state. -# -# This script requires cx_Oracle 7.1 or higher. -# -# Also see session_callback.py -# -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb -import sample_env - -# create pool with session callback defined -pool = oracledb.SessionPool(user=sample_env.get_main_user(), - password=sample_env.get_main_password(), - dsn=sample_env.get_connect_string(), min=2, max=5, - increment=1, - session_callback="pkg_SessionCallback.TheCallback") - -# truncate table logging calls to PL/SQL session callback -with pool.acquire() as conn: - cursor = conn.cursor() - cursor.execute("truncate table PLSQLSessionCallbacks") - -# acquire session without specifying a tag; the callback will not be invoked as -# a result and no session state will be changed -print("(1) acquire session without tag") -with pool.acquire() as conn: - cursor = conn.cursor() - cursor.execute("select to_char(current_date) from dual") - result, = cursor.fetchone() - print("main(): result is", repr(result)) - -# acquire session, specifying a tag; since the session returned has no tag, -# the callback will be invoked; session state will be changed and the tag will -# be saved when the connection is closed -print("(2) acquire session with tag") -with pool.acquire(tag="NLS_DATE_FORMAT=SIMPLE") as conn: - cursor = conn.cursor() - cursor.execute("select to_char(current_date) from dual") - result, = cursor.fetchone() - print("main(): result is", repr(result)) - -# acquire session, specifying the same tag; since a session exists in the pool -# with this tag, it will be returned and the callback will not be invoked but -# the connection will still have the session state defined previously -print("(3) acquire session with same tag") -with pool.acquire(tag="NLS_DATE_FORMAT=SIMPLE") as conn: - cursor = conn.cursor() - cursor.execute("select to_char(current_date) from dual") - result, = cursor.fetchone() - print("main(): result is", repr(result)) - -# acquire session, specifying a different tag; since no session exists in the -# pool with this tag, a new session will be returned and the callback will be -# invoked; session state will be changed and the tag will be saved when the -# connection is closed -print("(4) acquire session with different tag") -with pool.acquire(tag="NLS_DATE_FORMAT=FULL;TIME_ZONE=UTC") as conn: - cursor = conn.cursor() - cursor.execute("select to_char(current_date) from dual") - result, = cursor.fetchone() - print("main(): result is", repr(result)) - -# acquire session, specifying a different tag but also specifying that a -# session with any tag can be acquired from the pool; a session with one of the -# previously set tags will be returned and the callback will be invoked; -# session state will be changed and the tag will be saved when the connection -# is closed -print("(4) acquire session with different tag but match any also specified") -with pool.acquire(tag="NLS_DATE_FORMAT=FULL;TIME_ZONE=MST", matchanytag=True) \ - as conn: - cursor = conn.cursor() - cursor.execute("select to_char(current_date) from dual") - result, = cursor.fetchone() - print("main(): result is", repr(result)) - -# acquire session and display results from PL/SQL session logs -with pool.acquire() as conn: - cursor = conn.cursor() - cursor.execute(""" - select RequestedTag, ActualTag - from PLSQLSessionCallbacks - order by FixupTimestamp""") - print("(5) PL/SQL session callbacks") - for requestedTag, actualTag in cursor: - print("Requested:", requestedTag, "Actual:", actualTag) diff --git a/samples/setup_samples.py b/samples/setup_samples.py deleted file mode 100644 index 0925c13..0000000 --- a/samples/setup_samples.py +++ /dev/null @@ -1,32 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2019, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# setup_samples.py -# -# Creates users and populates their schemas with the tables and packages -# necessary for running the sample scripts. An edition is also created for the -# demonstration of PL/SQL editioning. -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb - -import sample_env -import drop_samples - -# connect as administrative user (usually SYSTEM or ADMIN) -conn = oracledb.connect(sample_env.get_admin_connect_string()) - -# drop existing users and editions, if applicable -drop_samples.drop_samples(conn) - -# create sample schema and edition -print("Creating sample schemas and edition...") -sample_env.run_sql_script(conn, "setup_samples", - main_user=sample_env.get_main_user(), - main_password=sample_env.get_main_password(), - edition_user=sample_env.get_edition_user(), - edition_password=sample_env.get_edition_password(), - edition_name=sample_env.get_edition_name()) -print("Done.") diff --git a/samples/sharding_number_key.py b/samples/sharding_number_key.py deleted file mode 100644 index a71ac1a..0000000 --- a/samples/sharding_number_key.py +++ /dev/null @@ -1,35 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# sharding_number_key.py -# This script demonstrates how to use sharding keys with a sharded database. -# The sample schema provided does not include support for running this demo. A -# sharded database must first be created. Information on how to create a -# sharded database can be found in the documentation: -# https://www.oracle.com/pls/topic/lookup?ctx=dblatest&id=SHARD -# -# This script requires cx_Oracle 6.1 and higher but it is recommended to use -# cx_Oracle 7.3 and higher in order to avoid a set of known issues when using -# sharding capabilities. -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb -import sample_env - -pool = oracledb.SessionPool(user=sample_env.get_main_user(), - password=sample_env.get_main_password(), - dsn=sample_env.get_connect_string(), min=1, max=5, - increment=1) - -def connect_and_display(sharding_key): - print("Connecting with sharding key:", sharding_key) - with pool.acquire(shardingkey=[sharding_key]) as conn: - cursor = conn.cursor() - cursor.execute("select sys_context('userenv', 'db_name') from dual") - name, = cursor.fetchone() - print("--> connected to database", name) - -connect_and_display(100) -connect_and_display(167) diff --git a/samples/soda_basic.py b/samples/soda_basic.py deleted file mode 100644 index a04713a..0000000 --- a/samples/soda_basic.py +++ /dev/null @@ -1,120 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2018, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# soda_basic.py -# A basic Simple Oracle Document Access (SODA) example. -# -# This script requires cx_Oracle 7.0 and higher. -# Oracle Client must be at 18.3 or higher. -# Oracle Database must be at 18.1 or higher. -# The user must have been granted the SODA_APP privilege. -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb -import sample_env - -connection = oracledb.connect(sample_env.get_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() - -# drop the collection if it already exists in order to ensure that the sample -# runs smoothly each time -collection = soda.openCollection("mycollection") -if collection is not None: - collection.drop() - -# Explicit metadata is used for maximum version portability. -# Refer to the documentation. -metadata = { - "keyColumn": { - "name": "ID" - }, - "contentColumn": { - "name": "JSON_DOCUMENT", - "sqlType": "BLOB" - }, - "versionColumn": { - "name": "VERSION", - "method": "UUID" - }, - "lastModifiedColumn": { - "name": "LAST_MODIFIED" - }, - "creationTimeColumn": { - "name": "CREATED_ON" - } -} - -# Create a new SODA collection and index -# This will open an existing collection, if the name is already in use. -collection = soda.createCollection("mycollection", metadata) - -index_spec = { - 'name': 'CITY_IDX', - 'fields': [ - { - 'path': 'address.city', - 'datatype': 'string', - 'order': 'asc' - } - ] -} -collection.createIndex(index_spec) - -# Insert a document. -# 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/soda_bulk_insert.py b/samples/soda_bulk_insert.py deleted file mode 100644 index 714276a..0000000 --- a/samples/soda_bulk_insert.py +++ /dev/null @@ -1,79 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2019, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# soda_bulk_insert.py -# Demonstrates the use of SODA bulk insert. -# -# This script requires cx_Oracle 7.2 and higher. -# Oracle Client must be at 18.5 or higher. -# Oracle Database must be at 18.1 or higher. -# The user must have been granted the SODA_APP privilege. -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb -import sample_env - -connection = oracledb.connect(sample_env.get_main_connect_string()) - -# the general recommendation for simple SODA usage is to enable autocommit -connection.autocommit = True - -# create the parent object for all SODA work -soda = connection.getSodaDatabase() - -# drop the collection if it already exists in order to ensure that the sample -# runs smoothly each time -collection = soda.openCollection("SodaBulkInsert") -if collection is not None: - collection.drop() - -# Explicit metadata is used for maximum version portability. -# Refer to the documentation. -metadata = { - "keyColumn": { - "name": "ID" - }, - "contentColumn": { - "name": "JSON_DOCUMENT", - "sqlType": "BLOB" - }, - "versionColumn": { - "name": "VERSION", - "method": "UUID" - }, - "lastModifiedColumn": { - "name": "LAST_MODIFIED" - }, - "creationTimeColumn": { - "name": "CREATED_ON" - } -} - -# create a new (or open an existing) SODA collection -collection = soda.createCollection("SodaBulkInsert", metadata) - -# remove all documents from the collection -collection.find().remove() - -# define some documents that will be stored -in_docs = [ - dict(name="Sam", age=8), - dict(name="George", age=46), - dict(name="Bill", age=35), - dict(name="Sally", age=43), - dict(name="Jill", age=28), - dict(name="Cynthia", age=12) -] - -# perform bulk insert -result_docs = collection.insertManyAndGet(in_docs) -for doc in result_docs: - print("Inserted SODA document with key", doc.key) -print() - -# perform search of all persons under the age of 40 -print("Persons under the age of 40:") -for doc in collection.find().filter({'age': {'$lt': 40}}).getDocuments(): - print(doc.getContent()["name"] + ",", "key", doc.key) diff --git a/samples/spatial_to_geopandas.py b/samples/spatial_to_geopandas.py deleted file mode 100644 index 1652e12..0000000 --- a/samples/spatial_to_geopandas.py +++ /dev/null @@ -1,186 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2018, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# spatial_to_geopandas.py -# GeoPandas is a popular python library for working with geospatial data. -# GeoPandas extends the Pandas data analysis library with geospatial support -# using the Shapely library for geometry object support. -# -# See http://geopandas.org, https://pandas.pydata.org, -# and https://github.com/Toblerity/Shapely. -# -# This example shows how to bring geometries from Oracle Spatial (SDO_GEOMETRY -# data type) into GeoPandas and perform a simple spatial operation. While the -# spatial operation we perform in Python could have been performed in the -# Oracle database, this example targets use cases where Python with GeoPandas -# is being used to combine and work with geospatial data from numerous -# additional sources such as files and web services. -# -# This script requires cx_Oracle (5.3 and higher) as well as GeoPandas and its -# dependencies (see http://geopandas.org/install.html). -#------------------------------------------------------------------------------ - -from shapely.wkb import loads -import geopandas as gpd - -import cx_Oracle as oracledb -import sample_env - -# create Oracle connection and cursor objects -connection = oracledb.connect(sample_env.get_main_connect_string()) -cursor = connection.cursor() - -# enable autocommit to avoid the additional round trip to the database to -# perform a commit; this should not be used if multiple statements must be -# executed for a single transaction -connection.autocommit = True - -# define output type handler to fetch LOBs, avoiding the second round trip to -# the database to read the LOB contents -def output_type_handler(cursor, name, default_type, size, precision, scale): - if default_type == oracledb.BLOB: - return cursor.var(oracledb.LONG_BINARY, arraysize=cursor.arraysize) -connection.outputtypehandler = output_type_handler - -# drop and create table -print("Dropping and creating table...") -cursor.execute(""" - begin - execute immediate 'drop table TestStates'; - exception when others then - if sqlcode <> -942 then - raise; - end if; - end;""") -cursor.execute(""" - create table TestStates ( - state VARCHAR2(30) not null, - geometry SDO_GEOMETRY not null - )""") - -# acquire types used for creating SDO_GEOMETRY objects -type_obj = connection.gettype("MDSYS.SDO_GEOMETRY") -element_info_type_obj = connection.gettype("MDSYS.SDO_ELEM_INFO_ARRAY") -ordinate_type_obj = connection.gettype("MDSYS.SDO_ORDINATE_ARRAY") - -# define function for creating an SDO_GEOMETRY object -def create_geometry_obj(*ordinates): - geometry = type_obj.newobject() - geometry.SDO_GTYPE = 2003 - geometry.SDO_SRID = 8307 - geometry.SDO_ELEM_INFO = element_info_type_obj.newobject() - geometry.SDO_ELEM_INFO.extend([1, 1003, 1]) - geometry.SDO_ORDINATES = ordinate_type_obj.newobject() - geometry.SDO_ORDINATES.extend(ordinates) - return geometry - -# create SDO_GEOMETRY objects for three adjacent states in the USA -geometry_nevada = create_geometry_obj(-114.052025, 37.103989, -114.049797, - 37.000423, -113.484375, 37, -112.898598, 37.000401,-112.539604, - 37.000683, -112, 37.000977, -111.412048, 37.001514, -111.133018, - 37.00079,-110.75, 37.003201, -110.5, 37.004265, -110.469505, 36.998001, - -110, 36.997967, -109.044571,36.999088, -109.045143, 37.375, - -109.042824, 37.484692, -109.040848, 37.881176, -109.041405,38.153027, - -109.041107, 38.1647, -109.059402, 38.275501, -109.059296, 38.5, - -109.058868, 38.719906,-109.051765, 39, -109.050095, 39.366699, - -109.050697, 39.4977, -109.050499, 39.6605, -109.050156,40.222694, - -109.047577, 40.653641, -109.0494, 41.000702, -109.2313, 41.002102, - -109.534233,40.998184, -110, 40.997398, -110.047768, 40.997696, -110.5, - 40.994801, -111.045982, 40.998013,-111.045815, 41.251774, -111.045097, - 41.579899, -111.045944, 42.001633, -111.506493, 41.999588,-112.108742, - 41.997677, -112.16317, 41.996784, -112.172562, 41.996643, -112.192184, - 42.001244,-113, 41.998314, -113.875, 41.988091, -114.040871, 41.993805, - -114.038803, 41.884899, -114.041306,41, -114.04586, 40.116997, - -114.046295, 39.906101, -114.046898, 39.542801, -114.049026, 38.67741, - -114.049339, 38.572968, -114.049095, 38.14864, -114.0476, - 37.80946,-114.05098, 37.746284, -114.051666, 37.604805, -114.052025, - 37.103989) -geometry_wyoming = create_geometry_obj(-111.045815, 41.251774, -111.045982, - 40.998013, -110.5, 40.994801, -110.047768, 40.997696, -110, 40.997398, - -109.534233, 40.998184, -109.2313, 41.002102, -109.0494, 41.000702, - -108.525368, 40.999634, -107.917793, 41.002071, -107.317177, 41.002956, - -106.857178, 41.002697, -106.455704, 41.002167, -106.320587, 40.999153, - -106.189987, 40.997604, -105.729874, 40.996906, -105.276604, 40.998188, - -104.942848, 40.998226, -104.625, 41.00145, -104.052742, 41.001423, - -104.051781, 41.39333, -104.052032, 41.564301, -104.052185, 41.697983, - -104.052109, 42.001736, -104.052277, 42.611626, -104.052643, 43.000614, - -104.054337, 43.47784, -104.054298, 43.503101, -104.055, 43.8535, - -104.054108, 44.141102, -104.054001, 44.180401, -104.055458, 44.570877, - -104.057205, 44.997444, -104.664658, 44.998631, -105.037872, 45.000359, - -105.088867, 45.000462, -105.912819, 45.000957, -105.927612, 44.99366, - -106.024239, 44.993591, -106.263, 44.993801, -107.054871, 44.996384, - -107.133545, 45.000141, -107.911095, 45.001343, -108.248672, 44.999504, - -108.620628, 45.000328, -109.082314, 44.999664, -109.102745, 45.005955, - -109.797951, 45.002247, -110.000771, 45.003502, -110.10936, 45.003967, - -110.198761, 44.99625, -110.286026, 44.99691, -110.361946, 45.000656, - -110.402176, 44.993874, -110.5, 44.992355, -110.704506, 44.99239, - -110.784241, 45.003021, -111.05442, 45.001392, -111.054558, 44.666336, - -111.048203, 44.474144, -111.046272, 43.983456, -111.044724, 43.501213, - -111.043846, 43.3158, -111.043381, 43.02013, -111.042786, 42.719578, - -111.045967, 42.513187, -111.045944, 42.001633, -111.045097, 41.579899, - -111.045815, 41.251774) -geometry_colorado = create_geometry_obj(-109.045143, 37.375, -109.044571, - 36.999088, -108.378571, 36.999516, -107.481133, 37, -107.420311, 37, - -106.876701, 37.00013, -106.869209, 36.992416, -106.475639, 36.993748, - -106.006058, 36.995327, -105.717834, 36.995823, -105.220055, 36.995144, - -105.154488, 36.995239, -105.028671, 36.992702, -104.407616, 36.993446, - -104.007324, 36.996216, -103.085617, 37.000244, -103.001709, 37.000084, - -102.986488, 36.998505, -102.759384, 37, -102.69767, 36.995132, - -102.041794, 36.993061, -102.041191, 37.389172, -102.04113, 37.644268, - -102.041695, 37.738529, -102.043938, 38.262466, -102.044113, 38.268803, - -102.04483, 38.615234, -102.044762, 38.697556, -102.046112, 39.047035, - -102.046707, 39.133144, -102.049301, 39.568176, -102.049347, 39.574062, - -102.051277, 40.00309, -102.051117, 40.34922, -102.051003, 40.440018, - -102.050873, 40.697556, -102.050835, 40.749596, -102.051155, 41.002384, - -102.620567, 41.002609, -102.652992, 41.002342, -103.382011, 41.00227, - -103.574036, 41.001736, -104.052742, 41.001423, -104.625, 41.00145, - -104.942848, 40.998226, -105.276604, 40.998188, -105.729874, 40.996906, - -106.189987, 40.997604, -106.320587, 40.999153, -106.455704, 41.002167, - -106.857178, 41.002697, -107.317177, 41.002956, -107.917793, 41.002071, - -108.525368, 40.999634, -109.0494, 41.000702, -109.047577, 40.653641, - -109.050156, 40.222694, -109.050499, 39.6605, -109.050697, 39.4977, - -109.050095, 39.366699, -109.051765, 39, -109.058868, 38.719906, - -109.059296, 38.5, -109.059402, 38.275501, -109.041107, 38.1647, - -109.041405, 38.153027, -109.040848, 37.881176, -109.042824, 37.484692, - -109.045143, 37.375) - -# Insert rows for test states. If we were analyzing these geometries in Oracle -# we would also add Spatial metadata and indexes. However in this example we -# are only storing the geometries so that we load them back into Python, so we -# will skip the metadata and indexes. -print("Adding rows to table...") -data = [ - ('Nevada', geometry_nevada), - ('Colorado', geometry_colorado), - ('Wyoming', geometry_wyoming) -] -cursor.executemany('insert into TestStates values (:state, :obj)', data) - -# We now have test geometries in Oracle Spatial (SDO_GEOMETRY) and will next -# bring them back into Python to analyze with GeoPandas. GeoPandas is able to -# consume geometries in the Well Known Text (WKT) and Well Known Binary (WKB) -# formats. Oracle database includes utility functions to return SDO_GEOMETRY as -# both WKT and WKB. Therefore we use that utility function in the query below -# to provide results in a format readily consumable by GeoPandas. These utility -# functions were introduced in Oracle 10g. We use WKB here; however the same -# process applies for WKT. -cursor.execute(""" - SELECT state, sdo_util.to_wkbgeometry(geometry) - FROM TestStates""") -gdf = gpd.GeoDataFrame(cursor.fetchall(), columns=['state', 'wkbgeometry']) - -# create GeoSeries to replace the WKB geometry column -gdf['geometry'] = gpd.GeoSeries(gdf['wkbgeometry'].apply(lambda x: loads(x))) -del gdf['wkbgeometry'] - -# display the GeoDataFrame -print() -print(gdf) - -# perform a basic GeoPandas operation (unary_union) -# to combine the 3 adjacent states into 1 geometry -print() -print("GeoPandas combining the 3 geometries into a single geometry...") -print(gdf.unary_union) diff --git a/samples/sql/drop_samples.sql b/samples/sql/drop_samples.sql deleted file mode 100644 index eca66a9..0000000 --- a/samples/sql/drop_samples.sql +++ /dev/null @@ -1,28 +0,0 @@ -/*----------------------------------------------------------------------------- - * Copyright 2017, 2019, Oracle and/or its affiliates. All rights reserved. - *---------------------------------------------------------------------------*/ - -/*----------------------------------------------------------------------------- - * drop_samples.sql - * Drops database objects used for cx_Oracle samples. - * - * Run this like: - * sqlplus sys/syspassword@hostname/servicename as sysdba @drop_samples - *---------------------------------------------------------------------------*/ - -whenever sqlerror exit failure - --- get parameters -set echo off termout on feedback off verify off -accept main_user char default pythondemo - - prompt "Name of main schema [pythondemo]: " -accept edition_user char default pythoneditions - - prompt "Name of edition schema [pythoneditions]: " -accept edition_name char default python_e1 - - prompt "Name of edition [python_e1]: " -set feedback on - --- perform work -@@drop_samples_exec.sql - -exit diff --git a/samples/sql/drop_samples_exec.sql b/samples/sql/drop_samples_exec.sql deleted file mode 100644 index dc0ab7a..0000000 --- a/samples/sql/drop_samples_exec.sql +++ /dev/null @@ -1,32 +0,0 @@ -/*----------------------------------------------------------------------------- - * Copyright 2017, 2019, Oracle and/or its affiliates. All rights reserved. - *---------------------------------------------------------------------------*/ - -/*----------------------------------------------------------------------------- - * drop_samples_exec.sql - * This script performs the actual work of dropping the database schemas and - * edition used by the cx_Oracle samples. It is called by the drop_samples.sql - * and setup_samples.sql files after acquiring the necessary parameters and - * also by the Python script drop_samples.py. - *---------------------------------------------------------------------------*/ - -begin - - for r in - ( select username - from dba_users - where username in (upper('&main_user'), upper('&edition_user')) - ) loop - execute immediate 'drop user ' || r.username || ' cascade'; - end loop; - - for r in - ( select edition_name - from dba_editions - where edition_name in (upper('&edition_name')) - ) loop - execute immediate 'drop edition ' || r.edition_name || ' cascade'; - end loop; - -end; -/ diff --git a/samples/sql/setup_samples.sql b/samples/sql/setup_samples.sql deleted file mode 100644 index 56c8554..0000000 --- a/samples/sql/setup_samples.sql +++ /dev/null @@ -1,33 +0,0 @@ -/*----------------------------------------------------------------------------- - * Copyright 2017, 2019, Oracle and/or its affiliates. All rights reserved. - *---------------------------------------------------------------------------*/ - -/*----------------------------------------------------------------------------- - * setup_samples.sql - * Creates and populates schemas with the database objects used by the - * cx_Oracle samples. An edition is also created for the demonstration of - * PL/SQL editioning. - * - * Run this like: - * sqlplus sys/syspassword@hostname/servicename as sysdba @setup_samples - *---------------------------------------------------------------------------*/ - -whenever sqlerror exit failure - --- get parameters -set echo off termout on feedback off verify off -accept main_user char default pythondemo - - prompt "Name of main schema [pythondemo]: " -accept main_password char prompt "Password for &main_user: " HIDE -accept edition_user char default pythoneditions - - prompt "Name of edition schema [pythoneditions]: " -accept edition_password char prompt "Password for &edition_user: " HIDE -accept edition_name char default python_e1 - - prompt "Name of edition [python_e1]: " -set feedback on - --- perform work -@@drop_samples_exec.sql -@@setup_samples_exec.sql - -exit diff --git a/samples/sql/setup_samples_exec.sql b/samples/sql/setup_samples_exec.sql deleted file mode 100644 index bb56277..0000000 --- a/samples/sql/setup_samples_exec.sql +++ /dev/null @@ -1,558 +0,0 @@ -/*----------------------------------------------------------------------------- - * Copyright 2017, 2020, Oracle and/or its affiliates. All rights reserved. - *---------------------------------------------------------------------------*/ - -/*----------------------------------------------------------------------------- - * setup_samples_exec.sql - * This script performs the actual work of creating and populating the - * schemas with the database objects used by the sample scripts. An edition - * is also created for the demonstration of PL/SQL editioning. It is called by - * the setup_samples.sql file after acquiring the necessary parameters and also - * by the Python script setup_samples.py. - *---------------------------------------------------------------------------*/ - -alter session set nls_date_format = 'YYYY-MM-DD HH24:MI:SS' -/ -alter session set nls_numeric_characters='.,' -/ - -create user &main_user identified by &main_password -/ - -grant - create session, - create table, - create procedure, - create type, - select any dictionary, - change notification, - unlimited tablespace -to &main_user -/ - -grant aq_administrator_role to &main_user -/ - -begin - execute immediate 'begin dbms_session.sleep(0); end;'; -exception -when others then - begin - execute immediate 'grant execute on dbms_lock to &main_user'; - exception - when others then - raise_application_error(-20000, - 'Ensure the following grant is made: ' || - 'grant execute on dbms_lock to ' || user || - ' with grant option'); - end; -end; -/ - -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 - create session, - create procedure -to &edition_user -/ - -alter user &edition_user enable editions -/ - -create edition &edition_name -/ - -grant use on edition &edition_name to &edition_user -/ - --- create types - -create type &main_user..udt_SubObject as object ( - SubNumberValue number, - SubStringValue varchar2(60) -); -/ - -create or replace type &main_user..udt_Building as object ( - BuildingId number(9), - NumFloors number(3), - Description varchar2(60), - DateBuilt date -); -/ - -create or replace type &main_user..udt_Book as object ( - Title varchar2(100), - Authors varchar2(100), - Price number(5,2) -); -/ - --- create tables - -create table &main_user..TestNumbers ( - IntCol number(9) not null, - NumberCol number(9, 2) not null, - FloatCol float not null, - UnconstrainedCol number not null, - NullableCol number(38) -) -/ - -create table &main_user..TestStrings ( - IntCol number(9) not null, - StringCol varchar2(20) not null, - RawCol raw(30) not null, - FixedCharCol char(40) not null, - NullableCol varchar2(50) -) -/ - -create table &main_user..TestCLOBs ( - IntCol number(9) not null, - CLOBCol clob not null -) -/ - -create table &main_user..TestBLOBs ( - IntCol number(9) not null, - BLOBCol blob not null -) -/ - -create table &main_user..TestTempTable ( - IntCol number(9) not null, - StringCol varchar2(400), - constraint TestTempTable_pk primary key (IntCol) -) -/ - -create table &main_user..TestUniversalRowids ( - IntCol number(9) not null, - StringCol varchar2(250) not null, - DateCol date not null, - constraint TestUniversalRowids_pk primary key (IntCol, StringCol, DateCol) -) organization index -/ - -create table &main_user..TestBuildings ( - BuildingId number(9) not null, - BuildingObj &main_user..udt_Building not null -) -/ - -create table &main_user..BigTab ( - mycol varchar2(20) -) -/ - -create table &main_user..SampleQueryTab ( - id number not null, - name varchar2(20) not null -) -/ - -create table &main_user..MyTab ( - id number, - data varchar2(20) -) -/ - -create table &main_user..ParentTable ( - ParentId number(9) not null, - Description varchar2(60) not null, - constraint ParentTable_pk primary key (ParentId) -) -/ - -create table &main_user..ChildTable ( - ChildId number(9) not null, - ParentId number(9) not null, - Description varchar2(60) not null, - constraint ChildTable_pk primary key (ChildId), - constraint ChildTable_fk foreign key (ParentId) - references &main_user..ParentTable -) -/ - -create table &main_user..Ptab ( - myid number, - mydata varchar(20) -) -/ - -create table &main_user..PlsqlSessionCallbacks ( - RequestedTag varchar2(250), - ActualTag varchar2(250), - FixupTimestamp timestamp -) -/ - --- create queue table, queues and subscribers for demonstrating Advanced Queuing -begin - - dbms_aqadm.create_queue_table('&main_user..BOOK_QUEUE_TAB', - '&main_user..UDT_BOOK'); - dbms_aqadm.create_queue('&main_user..DEMO_BOOK_QUEUE', - '&main_user..BOOK_QUEUE_TAB'); - dbms_aqadm.start_queue('&main_user..DEMO_BOOK_QUEUE'); - - dbms_aqadm.create_queue_table('&main_user..RAW_QUEUE_TAB', 'RAW'); - dbms_aqadm.create_queue('&main_user..DEMO_RAW_QUEUE', - '&main_user..RAW_QUEUE_TAB'); - dbms_aqadm.start_queue('&main_user..DEMO_RAW_QUEUE'); - - dbms_aqadm.create_queue_table('&main_user..RAW_QUEUE_MULTI_TAB', 'RAW', - multiple_consumers => true); - dbms_aqadm.create_queue('&main_user..DEMO_RAW_QUEUE_MULTI', - '&main_user..RAW_QUEUE_MULTI_TAB'); - dbms_aqadm.start_queue('&main_user..DEMO_RAW_QUEUE_MULTI'); - - dbms_aqadm.add_subscriber('&main_user..DEMO_RAW_QUEUE_MULTI', - sys.aq$_agent('SUBSCRIBER_A', null, null)); - dbms_aqadm.add_subscriber('&main_user..DEMO_RAW_QUEUE_MULTI', - sys.aq$_agent('SUBSCRIBER_B', null, null)); - -end; -/ - --- populate tables - -begin - for i in 1..20000 loop - insert into &main_user..BigTab (mycol) - values (dbms_random.string('A', 20)); - end loop; -end; -/ - -begin - for i in 1..10 loop - insert into &main_user..TestNumbers - values (i, i + i * 0.25, i + i * .75, i * i * i + i *.5, - decode(mod(i, 2), 0, null, power(143, i))); - end loop; -end; -/ - -declare - - t_RawValue raw(30); - - function ConvertHexDigit(a_Value number) return varchar2 is - begin - if a_Value between 0 and 9 then - return to_char(a_Value); - end if; - return chr(ascii('A') + a_Value - 10); - end; - - function ConvertToHex(a_Value varchar2) return varchar2 is - t_HexValue varchar2(60); - t_Digit number; - begin - for i in 1..length(a_Value) loop - t_Digit := ascii(substr(a_Value, i, 1)); - t_HexValue := t_HexValue || - ConvertHexDigit(trunc(t_Digit / 16)) || - ConvertHexDigit(mod(t_Digit, 16)); - end loop; - return t_HexValue; - end; - -begin - for i in 1..10 loop - t_RawValue := hextoraw(ConvertToHex('Raw ' || to_char(i))); - insert into &main_user..TestStrings - values (i, 'String ' || to_char(i), t_RawValue, - 'Fixed Char ' || to_char(i), - decode(mod(i, 2), 0, null, 'Nullable ' || to_char(i))); - end loop; -end; -/ - -insert into &main_user..ParentTable values (10, 'Parent 10') -/ -insert into &main_user..ParentTable values (20, 'Parent 20') -/ -insert into &main_user..ParentTable values (30, 'Parent 30') -/ -insert into &main_user..ParentTable values (40, 'Parent 40') -/ -insert into &main_user..ParentTable values (50, 'Parent 50') -/ - -insert into &main_user..ChildTable values (1001, 10, 'Child A of Parent 10') -/ -insert into &main_user..ChildTable values (1002, 20, 'Child A of Parent 20') -/ -insert into &main_user..ChildTable values (1003, 20, 'Child B of Parent 20') -/ -insert into &main_user..ChildTable values (1004, 20, 'Child C of Parent 20') -/ -insert into &main_user..ChildTable values (1005, 30, 'Child A of Parent 30') -/ -insert into &main_user..ChildTable values (1006, 30, 'Child B of Parent 30') -/ -insert into &main_user..ChildTable values (1007, 40, 'Child A of Parent 40') -/ -insert into &main_user..ChildTable values (1008, 40, 'Child B of Parent 40') -/ -insert into &main_user..ChildTable values (1009, 40, 'Child C of Parent 40') -/ -insert into &main_user..ChildTable values (1010, 40, 'Child D of Parent 40') -/ -insert into &main_user..ChildTable values (1011, 40, 'Child E of Parent 40') -/ -insert into &main_user..ChildTable values (1012, 50, 'Child A of Parent 50') -/ -insert into &main_user..ChildTable values (1013, 50, 'Child B of Parent 50') -/ -insert into &main_user..ChildTable values (1014, 50, 'Child C of Parent 50') -/ -insert into &main_user..ChildTable values (1015, 50, 'Child D of Parent 50') -/ - -insert into &main_user..SampleQueryTab values (1, 'Anthony') -/ -insert into &main_user..SampleQueryTab values (2, 'Barbie') -/ -insert into &main_user..SampleQueryTab values (3, 'Chris') -/ -insert into &main_user..SampleQueryTab values (4, 'Dazza') -/ -insert into &main_user..SampleQueryTab values (5, 'Erin') -/ -insert into &main_user..SampleQueryTab values (6, 'Frankie') -/ -insert into &main_user..SampleQueryTab values (7, 'Gerri') -/ - -commit -/ - --- --- For PL/SQL Examples --- - -create or replace function &main_user..myfunc ( - a_Data varchar2, - a_Id number -) return number as -begin - insert into &main_user..ptab (mydata, myid) values (a_Data, a_Id); - return (a_Id * 2); -end; -/ - -create or replace procedure &main_user..myproc ( - a_Value1 number, - a_Value2 out number -) as -begin - a_Value2 := a_Value1 * 2; -end; -/ - -create or replace procedure &main_user..myrefcursorproc ( - a_StartingValue number, - a_EndingValue number, - a_RefCursor out sys_refcursor -) as -begin - open a_RefCursor for - select * - from TestStrings - where IntCol between a_StartingValue and a_EndingValue; -end; -/ - -create procedure &main_user..myrefcursorproc2 ( - a_RefCursor out sys_refcursor -) as -begin - open a_RefCursor for - select * - from TestTempTable; -end; -/ - --- --- Create package for demoing PL/SQL collections and records. --- - -create or replace package &main_user..pkg_Demo as - - type udt_StringList is table of varchar2(100) index by binary_integer; - - type udt_DemoRecord is record ( - NumberValue number, - StringValue varchar2(30), - DateValue date, - BooleanValue boolean - ); - - procedure DemoCollectionOut ( - a_Value out nocopy udt_StringList - ); - - procedure DemoRecordsInOut ( - a_Value in out nocopy udt_DemoRecord - ); - -end; -/ - -create or replace package body &main_user..pkg_Demo as - - procedure DemoCollectionOut ( - a_Value out nocopy udt_StringList - ) is - begin - a_Value(-1048576) := 'First element'; - a_Value(-576) := 'Second element'; - a_Value(284) := 'Third element'; - a_Value(8388608) := 'Fourth element'; - end; - - procedure DemoRecordsInOut ( - a_Value in out nocopy udt_DemoRecord - ) is - begin - a_Value.NumberValue := a_Value.NumberValue * 2; - a_Value.StringValue := a_Value.StringValue || ' (Modified)'; - a_Value.DateValue := a_Value.DateValue + 5; - a_Value.BooleanValue := not a_Value.BooleanValue; - end; - -end; -/ - --- --- Create package for demoing PL/SQL session callback --- - -create or replace package &main_user..pkg_SessionCallback as - - procedure TheCallback ( - a_RequestedTag varchar2, - a_ActualTag varchar2 - ); - -end; -/ - -create or replace package body &main_user..pkg_SessionCallback as - - type udt_Properties is table of varchar2(64) index by varchar2(64); - - procedure LogCall ( - a_RequestedTag varchar2, - a_ActualTag varchar2 - ) is - pragma autonomous_transaction; - begin - insert into PlsqlSessionCallbacks - values (a_RequestedTag, a_ActualTag, systimestamp); - commit; - end; - - procedure ParseProperty ( - a_Property varchar2, - a_Name out nocopy varchar2, - a_Value out nocopy varchar2 - ) is - t_Pos number; - begin - t_Pos := instr(a_Property, '='); - if t_Pos = 0 then - raise_application_error(-20000, 'Tag must contain key=value pairs'); - end if; - a_Name := substr(a_Property, 1, t_Pos - 1); - a_Value := substr(a_Property, t_Pos + 1); - end; - - procedure SetProperty ( - a_Name varchar2, - a_Value varchar2 - ) is - t_ValidValues udt_Properties; - begin - if a_Name = 'TIME_ZONE' then - t_ValidValues('UTC') := 'UTC'; - t_ValidValues('MST') := '-07:00'; - elsif a_Name = 'NLS_DATE_FORMAT' then - t_ValidValues('SIMPLE') := 'YYYY-MM-DD HH24:MI'; - t_ValidValues('FULL') := 'YYYY-MM-DD HH24:MI:SS'; - else - raise_application_error(-20000, 'Unsupported session setting'); - end if; - if not t_ValidValues.exists(a_Value) then - raise_application_error(-20000, 'Unsupported session setting'); - end if; - execute immediate - 'ALTER SESSION SET ' || a_Name || '=''' || - t_ValidValues(a_Value) || ''''; - end; - - procedure ParseTag ( - a_Tag varchar2, - a_Properties out nocopy udt_Properties - ) is - t_PropertyName varchar2(64); - t_PropertyValue varchar2(64); - t_StartPos number; - t_EndPos number; - begin - t_StartPos := 1; - while t_StartPos < length(a_Tag) loop - t_EndPos := instr(a_Tag, ';', t_StartPos); - if t_EndPos = 0 then - t_EndPos := length(a_Tag) + 1; - end if; - ParseProperty(substr(a_Tag, t_StartPos, t_EndPos - t_StartPos), - t_PropertyName, t_PropertyValue); - a_Properties(t_PropertyName) := t_PropertyValue; - t_StartPos := t_EndPos + 1; - end loop; - end; - - procedure TheCallback ( - a_RequestedTag varchar2, - a_ActualTag varchar2 - ) is - t_RequestedProps udt_Properties; - t_ActualProps udt_Properties; - t_PropertyName varchar2(64); - begin - LogCall(a_RequestedTag, a_ActualTag); - ParseTag(a_RequestedTag, t_RequestedProps); - ParseTag(a_ActualTag, t_ActualProps); - t_PropertyName := t_RequestedProps.first; - while t_PropertyName is not null loop - if not t_ActualProps.exists(t_PropertyName) or - t_ActualProps(t_PropertyName) != - t_RequestedProps(t_PropertyName) then - SetProperty(t_PropertyName, t_RequestedProps(t_PropertyName)); - end if; - t_PropertyName := t_RequestedProps.next(t_PropertyName); - end loop; - end; - -end; -/ diff --git a/samples/subclassing.py b/samples/subclassing.py deleted file mode 100644 index 150f0f7..0000000 --- a/samples/subclassing.py +++ /dev/null @@ -1,53 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# subclassing.py -# -# Demonstrate how to subclass connections and cursors in order to add -# additional functionality (like logging) or create specialized interfaces for -# paticular applications. -#------------------------------------------------------------------------------ - -import cx_Oracle as oracledb -import sample_env - -# sample subclassed connection which overrides the constructor (so no -# parameters are required) and the cursor() method (so that the subclassed -# cursor is returned instead of the default cursor implementation) -class Connection(oracledb.Connection): - - def __init__(self): - connect_string = sample_env.get_main_connect_string() - print("CONNECT to database") - super().__init__(connect_string) - - def cursor(self): - return Cursor(self) - - -# sample subclassed cursor which overrides the execute() and fetchone() -# methods in order to perform some simple logging -class Cursor(oracledb.Cursor): - - def execute(self, statement, args): - print("EXECUTE", statement) - print("ARGS:") - for arg_index, arg in enumerate(args): - print(" ", arg_index + 1, "=>", repr(arg)) - return super().execute(statement, args) - - def fetchone(self): - print("FETCH ONE") - return super().fetchone() - - -# create instances of the subclassed connection and cursor -connection = Connection() -cursor = connection.cursor() - -# demonstrate that the subclassed connection and cursor are being used -cursor.execute("select count(*) from ChildTable where ParentId = :1", (30,)) -count, = cursor.fetchone() -print("COUNT:", int(count)) diff --git a/samples/transaction_guard.py b/samples/transaction_guard.py deleted file mode 100644 index 82284c0..0000000 --- a/samples/transaction_guard.py +++ /dev/null @@ -1,77 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved. -# -# Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved. -# -# Portions Copyright 2001-2007, Computronix (Canada) Ltd., Edmonton, Alberta, -# Canada. All rights reserved. -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# transaction_guard.py -# This script demonstrates the use of Transaction Guard to verify if a -# transaction has completed, ensuring that a duplicate transaction is not -# created or attempted if the application chooses to handle the error. This -# feature is only available in Oracle Database 12.1. It follows loosely the -# OCI sample provided by Oracle in its documentation about OCI and Transaction -# Guard. -# -# Run the following as SYSDBA to set up Transaction Guard -# -# grant execute on dbms_app_cont to pythondemo; -# -# declare -# t_Params dbms_service.svc_parameter_array; -# begin -# t_Params('COMMIT_OUTCOME') := 'true'; -# t_Params('RETENTION_TIMEOUT') := 604800; -# dbms_service.create_service('orcl-tg', 'orcl-tg', t_Params); -# dbms_service.start_service('orcl-tg'); -# end; -# / -# -# This script requires cx_Oracle 5.3 and higher. -#------------------------------------------------------------------------------ - -import sys - -import cx_Oracle as oracledb -import sample_env - -# constants -CONNECT_STRING = "localhost/orcl-tg" - -# create transaction and generate a recoverable error -pool = oracledb.SessionPool(user=sample_env.get_main_user(), - password=sample_env.get_main_password(), - dsn=CONNECT_STRING, min=1, max=9, increment=2) -connection = pool.acquire() -cursor = connection.cursor() -cursor.execute(""" - delete from TestTempTable - where IntCol = 1""") -cursor.execute(""" - insert into TestTempTable - values (1, null)""") -input("Please kill %s session now. Press ENTER when complete." % \ - sample_env.get_main_user()) -try: - connection.commit() # this should fail - sys.exit("Session was not killed. Terminating.") -except oracledb.DatabaseError as e: - error_obj, = e.args - if not error_obj.isrecoverable: - sys.exit("Session is not recoverable. Terminating.") -ltxid = connection.ltxid -if not ltxid: - sys.exit("Logical transaction not available. Terminating.") -pool.drop(connection) - -# check if previous transaction completed -connection = pool.acquire() -cursor = connection.cursor() -args = (oracledb.Binary(ltxid), cursor.var(bool), cursor.var(bool)) -_, committed, completed = cursor.callproc("dbms_app_cont.get_ltxid_outcome", - args) -print("Failed transaction was committed:", committed) -print("Failed call was completed:", completed) diff --git a/samples/tutorial/Python-and-Oracle-Database-Scripting-for-the-Future.html b/samples/tutorial/Python-and-Oracle-Database-Scripting-for-the-Future.html deleted file mode 100644 index dd677a0..0000000 --- a/samples/tutorial/Python-and-Oracle-Database-Scripting-for-the-Future.html +++ /dev/null @@ -1,2693 +0,0 @@ - - - -Python and Oracle Database Tutorial: Scripting for the Future - - - - - - - -

Python and Oracle Database Tutorial: Scripting for the Future

- -
- -

cx_Oracle has a major new release under a new name and - homepage python-oracledb.

- -

We recommend you use the new python-oracledb tutorial instead of this cx_Oracle tutorial.

- -
- - -

Contents

- -
    -
  • Overview
  • -
  • Setup
  • -
  • Connecting to Oracle -
      -
    • 1.1 Creating a basic connection
    • -
    • 1.2 Indentation indicates code structure
    • -
    • 1.3 Executing a query
    • -
    • 1.4 Closing connections
    • -
    • 1.5 Checking versions
    • -
    -
  • -
  • Connection Pooling -
      -
    • 2.1 Connection pooling
    • -
    • 2.2 Connection pool experiments
    • -
    • 2.3 Creating a DRCP Connection
    • -
    • 2.4 Connection pooling and DRCP
    • -
    • 2.5 More DRCP investigation
    • -
    -
  • -
  • Fetching Data -
      -
    • 3.1 A simple query
    • -
    • 3.2 Using fetchone()
    • -
    • 3.3 Using fetchmany()
    • -
    • 3.4 Scrollable cursors
    • -
    • 3.5 Tuning with arraysize and prefetchrows
    • -
    -
  • -
  • Binding Data -
      -
    • 4.1 Binding in queries
    • -
    • 4.2 Binding in inserts
    • -
    • 4.3 Batcherrors
    • -
    • 4.4 Binding named objects
    • -
    -
  • -
  • PL/SQL -
      -
    • 5.1 PL/SQL functions
    • -
    • 5.2 PL/SQL procedures
    • -
    -
  • -
  • Type Handlers -
      -
    • 6.1 Basic output type handler
    • -
    • 6.2 Output type handlers and variable converters
    • -
    • 6.3 Input type handlers
    • -
    -
  • -
  • LOBs -
      -
    • 7.1 Fetching a CLOB using a locator
    • -
    • 7.2 Fetching a CLOB as a string
    • -
    -
  • -
  • Rowfactory functions -
      -
    • 8.1 Rowfactory for mapping column names
    • -
    -
  • -
  • Subclassing connections and cursors -
      -
    • 9.1 Subclassing connections
    • -
    • 9.2 Subclassing cursors
    • -
    -
  • -
  • Advanced Queuing -
      -
    • 10.1 Message passing with Oracle Advanced Queuing
    • -
    -
  • -
  • Simple Oracle Document Access (SODA) -
      -
    • 11.1 Inserting JSON Documents
    • -
    • 11.2 Searching SODA Documents
    • -
    -
  • -
  • Summary
  • -
  • Appendix: Python Primer
  • -
  • Resources
  • -
- - -

Overview

- -

This tutorial is an introduction to using Python with Oracle Database. It - contains beginner and advanced material. Sections can be done in any order. - Choose the content that interests you and your skill level. The tutorial has - scripts to run and modify, and has suggested solutions.

- -

Python is a popular general purpose dynamic scripting language. The - cx_Oracle interface provides the Python API to access Oracle Database.

- -

If you are new to Python review the Appendix: - Python Primer to gain an understanding of the language.

- -

When you have finished this tutorial, we recommend reviewing the cx_Oracle - documention.

- -

The original copy of these instructions that you are reading is here.

- -

cx_Oracle Architecture

- -

Python programs call cx_Oracle functions. Internally cx_Oracle dynamically - loads Oracle Client libraries to access Oracle Database. The database can be - on the same machine as Python, or it can be remote. If the database is local, - the client libraries from the Oracle Database software installation can be - used.

- - Python cx_Oracle architecture - - -

Setup

- -
    - -
  • -

    Install software

    - -

    To get going, follow either of the quick start instructions:

    - - - - -

    For this tutorial, you will need Python 3.6 (or later), cx_Oracle 7.3 (or later), and access to Oracle Database.

    - -

    The Advanced Queuing section requires Python cx_Oracle to be using - Oracle client libraries 12.2 or later. The SODA section requires Oracle - Database 18 or later, and Python cx_Oracle must be using Oracle libraries - from 18.5, or later.

    - -
  • - - -
  • -

    Download the tutorial scripts

    - -

    The Python scripts used in this example are in the cx_Oracle GitHub repository.

    - -

    Download a zip file of the repository from here and unzip it. Alternatively you can use 'git' to clone the repository with git clone https://github.com/oracle/python-cx_Oracle.git

    - -

    The samples/tutorial directory has scripts to run and - modify. The samples/tutorial/solutions directory has scripts - with suggested code changes.

    - -
  • - -
  • Create a database user

    - -

    If you have an existing user, you may be able to use it for most - examples (some examples may require extra permissions).

    - -

    If you need to create a new user, review the grants created in - samples/tutorial/sql/create_user.sql. Then open a terminal - window, change to the samples/tutorial/sql directory, and - run the create_user.sql script as the SYSTEM user, for - example:

    - -
    -cd samples/tutorial/sql
    -sqlplus -l system/systempassword@localhost/orclpdb1 @create_user
    -
    - -

    The example above connects as the SYSTEM user. The connection - string is "localhost/orclpdb1", meaning use the database service - "orclpdb1" running on localhost (the computer you are running SQL*Plus - on). Substitute values for your environment. If you are using Oracle - Autonomous Database, use the ADMIN user instead of SYSTEM.

    - -

    When the tutorial is finished, the drop_user.sql script - in the same directory can be used to remove the tutorial user.

    - -
  • - -
  • Install the sample tables

    - -

    Once you have a database user, then you can create the tutorial - tables by running a command like this, using your values for the - tutorial username, password and connection string:

    - -
    -sqlplus -l pythonhol/welcome@localhost/orclpdb1 @setup_tables
    -
    - -
  • - -
  • Start the Database Resident Connection Pool (DRCP)

    - -

    If you want to try the DRCP examples in section 2, start the DRCP - pool. (The pool is already started in Oracle Autonomous Database).

    - -

    Run SQL*Plus with SYSDBA privileges, for example:

    - -
    -sqlplus -l sys/syspassword@localhost/orclcdb as sysdba
    -
    - -

    and execute the command:

    - -
    -execute dbms_connection_pool.start_pool()
    -
    - -

    Note you may need to do this in the container database, not a pluggable database.

    - -
  • -
  • -

    Review the connection credentials used by the tutorial scripts

    - -

    Review db_config.py and db_config.sql in - the tutorial directory. These are included in other - Python and SQL files.

    - -

    Edit db_config.py and change the default values to - match the connection information for your environment. Alternatively - you can set the given envionment variables in your terminal - window. For example, the default username is "pythonhol" unless the - envionment variable "PYTHON_USER" contains a different username. The - default connection string is for the 'orclpdb1' database service on - the same machine as Python. (In Python Database API terminology, the - connection string parameter is called the "data source name", or - "dsn".) Using envionment variables is convenient because you will not - be asked to re-enter the password when you run scripts:

    - -
    -user = os.environ.get("PYTHON_USER", "pythonhol")
    -
    -dsn = os.environ.get("PYTHON_CONNECT_STRING", "localhost/orclpdb1")
    -
    -pw = os.environ.get("PYTHON_PASSWORD")
    -if pw is None:
    -    pw = getpass.getpass("Enter password for %s: " % user)
    -
    - -

    Also change the default username and connection string in the SQL*Plus -configuration file db_config.sql:

    - -
    --- Default database username
    -def user = "pythonhol"
    -
    --- Default database connection string
    -def connect_string = "localhost/orclpdb1"
    -
    --- Prompt for the password
    -accept pw char prompt 'Enter database password for &user: ' hide
    -
    - - -

    The tutorial instructions may need adjusting, depending on how you - have set up your environment.

    -
  • - -
  • -

    Review the Instant Client library path

    - -

    Review the Oracle Client library path settings in - db_config.py. If cx_Oracle cannot locate Oracle Client - libraries, then your applications will fail with an error like - "DPI-1047: Cannot locate a 64-bit Oracle Client library".

    - -
    -# On Linux this must be None.
    -# Instead, the Oracle environment must be set before Python starts.
    -instant_client_dir = None
    -
    -# On Windows, if your database is on the same machine, comment these lines out
    -# and let instant_client_dir be None.  Otherwise, set this to your Instant
    -# Client directory.  Note the use of the raw string r"..."  so backslashes can
    -# be used as directory separators.
    -if sys.platform.startswith("win"):
    -    instant_client_dir = r"c:\oracle\instantclient_19_10"
    -
    -# On macOS (Intel x86) set the directory to your Instant Client directory
    -if sys.platform.startswith("darwin"):
    -    instant_client_dir = os.environ.get("HOME")+"/Downloads/instantclient_19_8"
    -
    -# This can be called at most once per process.
    -if instant_client_dir is not None:
    -    cx_Oracle.init_oracle_client(lib_dir=instant_client_dir)
    -
    - -

    Set instant_client_dir to None or to a - valid path according to the following notes:

    - -
      -
    • - -

      If you are on macOS or Windows, and you have installed Oracle - Instant Client libraries because your database is on a remote - machine, then set instant_client_dir to the path of - the Instant Client libraries.

      - -
    • - -
    • - -

      If you are on Windows and have a local database installed, then - comment out the two Windows lines, so that - instant_client_dir remains None.

      - -
    • - -
    • - -

      In all other cases (including Linux with Oracle Instant - Client), make sure that instant_client_dir is set to - None. In these cases you must make sure that the - Oracle libraries from Instant Client or your ORACLE_HOME are in - your system library search path before you start Python. On Linux, - the path can be configured with ldconfig or with the - LD_LIBRARY_PATH environment variables.

      - -
    • - -
    - -
  • -
- -

1. Connecting to Oracle

- -

You can connect from Python to a local, remote or cloud database. Documentation -link for further reading: Connecting to Oracle Database.

- -
    -
  • -

    1.1 Creating a basic connection

    -

    Review the code contained in connect.py:

    -
    -import cx_Oracle
    -import db_config
    -
    -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn)
    -print("Database version:", con.version)
    -
    - -

    The cx_Oracle module is imported to provide the API for accessing - the Oracle database. Many inbuilt and third party modules can be - included in Python scripts this way.

    - -

    The connect() method is passed the username, the - password and the connection string that you configured in the - db_config.py module. In this case, Oracle's Easy Connect connection - string syntax is used. It consists of the hostname of your machine, - localhost, and the database service name - orclpdb1. (In Python Database API terminology, the - connection string parameter is called the "data source name", or - "dsn".)

    - -

    Open a command terminal and change to the tutorial directory:

    - -
    cd samples/tutorial
    - -

    Run the Python script:

    - -
    python connect.py
    - -

    The version number of the database should be displayed. An - exception is raised if the connection fails. Adjust the username, - password or connection string parameters to invalid values to see the - exception.

    - -

    cx_Oracle also supports "external authentication", which allows - connections without needing usernames and passwords to be embedded in - the code. Authentication would then instead be performed by, for - example, LDAP or Oracle Wallets.

    - -
  • - -
  • -

    1.2 Indentation indicates code structure

    - -

    There are no statement terminators or begin/end keywords - or braces to indicate blocks of code.

    - -

    Open connect.py in an editor. Indent the - print statement with some spaces:

    - -
    -import cx_Oracle
    -import db_config
    -
    -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn)
    -  print("Database version:", con.version)
    -
    - -

    Save the script and run it again:

    - -
    python connect.py 
    - -

    This raises an exception about the indentation. The - number of spaces or tabs must be consistent in each block; - otherwise, the Python interpreter will either raise an - exception or execute code unexpectedly.

    - -

    Python may not always be able to identify accidental - from deliberate indentation. Check your indentation is - correct before running each example. Make sure to indent - all statement blocks equally. Note the sample files - use spaces, not tabs.

    - -
  • - -
  • -

    1.3 Executing a query

    - -

    Open query.py in an editor. It looks like:

    - -
    -import cx_Oracle
    -import db_config
    -
    -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn)
    -
    - -

    Edit the file and add the code shown in bold below:

    - -
    -import cx_Oracle
    -import db_config
    -
    -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn)
    -
    -cur = con.cursor()
    -cur.execute("select * from dept order by deptno")
    -res = cur.fetchall()
    -for row in res:
    -    print(row)
    -
    - -

    Make sure the print(row) line is indented. This lab uses spaces, not tabs.

    - -

    The code executes a query and fetches all data.

    - -

    Save the file and run it:

    - -
    python query.py
    - -

    In each loop iteration a new row is stored in - row as a Python "tuple" and is displayed.

    - -

    Fetching data is described further in section 3.

    -
  • - -
  • -

    1.4 Closing connections

    - -

    Connections and other resources used by cx_Oracle will - automatically be closed at the end of scope. This is a - common programming style that takes care of the correct - order of resource closure.

    - -

    Resources can also be explicitly closed to free up database - resources if they are no longer needed. This is strongly recommended - in blocks of code that remain active for some time.

    - -

    Open query.py in an editor and add calls to - close the cursor and connection like:

    - -
    -import cx_Oracle
    -import db_config
    -
    -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn)
    -
    -cur = con.cursor()
    -cur.execute("select * from dept order by deptno")
    -res = cur.fetchall()
    -for row in res:
    -    print(row)
    -
    -cur.close()
    -con.close()
    -
    - -

    Running the script completes without error:

    - -
    python query.py
    - -

    If you swap the order of the two close() calls you will see an error.

    -
  • - -
  • -

    1.5 Checking versions

    - -

    Review the code contained in versions.py:

    - -
    -import cx_Oracle
    -import db_config
    -
    -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn)
    -
    -print(cx_Oracle.version)
    - -

    Run the script:

    - -
    python versions.py
    - -

    This gives the version of the cx_Oracle interface.

    - -

    Edit the file to print the version of the database, and of the Oracle client libraries used by cx_Oracle:

    - -
    -import cx_Oracle
    -import db_config
    -
    -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn)
    -
    -print(cx_Oracle.version)
    -print("Database version:", con.version)
    -print("Client version:", cx_Oracle.clientversion())
    -
    - -

    When the script is run, it will display:

    - -
    -7.1.0
    -Database version: 19.3.0.0.0
    -Client version: (19, 8, 0, 0, 0)
    -
    - -

    Note the client version is a tuple.

    - -

    Any cx_Oracle installation can connect to older and newer - Oracle Database versions. By checking the Oracle Database - and client versions numbers, the application can make use of - the best Oracle features available.

    - -
  • - -
- -

2. Connection Pooling

- -

Connection pooling is important for performance in when multi-threaded - applications frequently connect and disconnect from the database. Pooling - also gives the best support for Oracle high availability features. - Documentation link for further reading: Connection Pooling.

- -
    -
  • 2.1 Connection pooling

    - -

    Review the code contained in connect_pool.py:

    -
    -import cx_Oracle
    -import threading
    -import db_config
    -
    -pool = cx_Oracle.SessionPool(db_config.user, db_config.pw, db_config.dsn,
    -                             min = 2, max = 5, increment = 1, threaded = True,
    -                             getmode = cx_Oracle.SPOOL_ATTRVAL_WAIT)
    -
    -def Query():
    -    con = pool.acquire()
    -    cur = con.cursor()
    -    for i in range(4):
    -        cur.execute("select myseq.nextval from dual")
    -        seqval, = cur.fetchone()
    -        print("Thread", threading.current_thread().name, "fetched sequence =", seqval)
    -
    -thread1 = threading.Thread(name='#1', target=Query)
    -thread1.start()
    -
    -thread2 = threading.Thread(name='#2', target=Query)
    -thread2.start()
    -
    -thread1.join()
    -thread2.join()
    -
    -print("All done!")
    -
    - -

    The SessionPool() function creates a pool of - Oracle connections for the user. Connections in the pool can - be used by cx_Oracle by calling pool.acquire(). - The initial pool size is 2 connections. The maximum size is 5 - connections. When the pool needs to grow, then 1 new connection - will be created at a time. The pool can shrink back to the - minimum size of 2 when connections are no longer in use.

    - -

    The def Query(): line creates a method that - is called by each thread.

    - -

    In the method, the pool.acquire() call gets - one connection from the pool (as long as less than 5 are - already in use). This connection is used in a loop of 4 - iterations to query the sequence myseq. At the - end of the method, cx_Oracle will automatically close the - cursor and release the connection back to the pool for - reuse.

    - -

    The seqval, = cur.fetchone() line fetches a - row and puts the single value contained in the result tuple - into the variable seqval. Without the comma, - the value in seqval would be a tuple like - "(1,)".

    - -

    Two threads are created, each invoking the - Query() method.

    - -

    In a command terminal, run:

    - -
    python connect_pool.py
    - -

    The output shows interleaved query results as each thread fetches -values independently. The order of interleaving may vary from run to -run.

    - -
  • - -
  • -

    2.2 Connection pool experiments

    - - -

    Review connect_pool2.py, which has a loop for the number -of threads, each iteration invoking the Query() method:

    - -
    -import cx_Oracle
    -import threading
    -import db_config
    -
    -pool = cx_Oracle.SessionPool(db_config.user, db_config.pw, db_config.dsn,
    -                             min = 2, max = 5, increment = 1, threaded = True,
    -                             getmode = cx_Oracle.SPOOL_ATTRVAL_WAIT)
    -
    -def Query():
    -    con = pool.acquire()
    -    cur = con.cursor()
    -    for i in range(4):
    -        cur.execute("select myseq.nextval from dual")
    -        seqval, = cur.fetchone()
    -        print("Thread", threading.current_thread().name, "fetched sequence =", seqval)
    -
    -numberOfThreads = 2
    -threadArray = []
    -
    -for i in range(numberOfThreads):
    -    thread = threading.Thread(name = '#' + str(i), target = Query)
    -    threadArray.append(thread)
    -    thread.start()
    -
    -for t in threadArray:
    -    t.join()
    -
    -print("All done!")
    -
    - -

    In a command terminal, run:

    - -
    python connect_pool2.py
    - -

    Experiment with different values of the pool parameters and -numberOfThreads. Larger initial pool sizes will make the pool -creation slower, but the connections will be available immediately when needed. -

    - -

    Try changing getmode to -cx_Oracle.SPOOL_ATTRVAL_NOWAIT. When numberOfThreads -exceeds the maximum size of the pool, the acquire() call will now -generate an error such as "ORA-24459: OCISessionGet() timed out waiting for pool -to create new connections".

    - -

    Pool configurations where min is the same as -max (and increment = 0) are often -recommended as a best practice. This avoids connection storms on the -database server.

    - -
  • - -
  • -

    2.3 Creating a DRCP Connection

    - -

    Database Resident Connection Pooling allows multiple Python - processes on multiple machines to share a small pool of database - server processes.

    - -

    Below left is a diagram without DRCP. Every application standalone - connection (or cx_Oracle connection-pool connection) has its own database - server process. Standalone application connect() and close calls - require the expensive create and destroy of those database server processes. - cx_Oracle connection pools reduce these costs by keeping database server - processes open, but every cx_Oracle connection pool will requires its own set - of database server processes, even if they are not doing database work: these - idle server processes consumes database host resources. Below right is a - diagram with DRCP. Scripts and Python processes can share database servers - from a precreated pool of servers and return them when they are not in use. -

    - - - - - - -
    - Picture of 3-tier application architecture without DRCP showing connections from multiple application processes each going to a server process in the database tier -

    Without DRCP

    -
    - Picture of 3-tier application architecture with DRCP showing connections from multiple application processes going to a pool of server processes in the database tier -

    With DRCP

    -
    - -

    DRCP is useful when the database host machine does not have enough memory - to handle the number of database server processes required. If DRCP is - enabled, it is best used in conjunction with cx_Oracle's connection pooling. - However, if the database host memory is large enough, then the default, - 'dedicated' server process model is generally recommended. This can be with - or without a cx_Oracle connection pool, depending on the connection rate.

    - -

    Batch scripts doing long running jobs should generally use - dedicated connections. Both dedicated and DRCP servers can be used - together in the same application or database.

    - -

    Review the code contained in connect_drcp.py:

    - -
    -import cx_Oracle
    -import db_config
    -
    -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn + ":pooled",
    -                        cclass="PYTHONHOL", purity=cx_Oracle.ATTR_PURITY_SELF)
    -print("Database version:", con.version)
    -
    - -

    This is similar to connect.py but - ":pooled" is appended to the connection string, telling - the database to use a pooled server. A Connection Class "PYTHONHOL" is also - passed into the connect() method to allow grouping of database - servers to applications. Note with Autonomous Database, the connection string - has a different form, see the ADB documentation.

    - -

    The "purity" of the connection is defined as the - ATTR_PURITY_SELF constant, meaning the session state - (such as the default date format) might be retained between - connection calls, giving performance benefits. Session information - will be discarded if a pooled server is later reused by an - application with a different connection class name.

    - -

    Applications that should never share session information should - use a different connection class and/or use - ATTR_PURITY_NEW to force creation of a new - session. This reduces overall scalability but prevents applications - mis-using session information.

    - -

    Run connect_drcp.py in a terminal window.

    - -
    python connect_drcp.py
    - -

    The output is simply the version of the database.

    - -
  • - -
  • -

    2.4 Connection pooling and DRCP

    - -

    DRCP works well with cx_Oracle's connection pooling.

    - -

    Edit connect_pool2.py, reset any changed pool options, and modify it to use DRCP:

    -
    -import cx_Oracle
    -import threading
    -
    -pool = cx_Oracle.SessionPool(db_config.user, db_config.pw, db_config.dsn + ":pooled",
    -                             min = 2, max = 5, increment = 1, threaded = True,
    -                             getmode = cx_Oracle.SPOOL_ATTRVAL_WAIT)
    -
    -def Query():
    -    con = pool.acquire(cclass = "PYTHONHOL", purity = cx_Oracle.ATTR_PURITY_SELF)
    -    cur = conn.cursor()
    -    for i in range(4):
    -        cur.execute("select myseq.nextval from dual")
    -        seqval, = cur.fetchone()
    -        print("Thread", threading.current_thread().name, "fetched sequence =", seqval)
    -
    -numberOfThreads = 2
    -threadArray = []
    -
    -for i in range(numberOfThreads):
    -    thread = threading.Thread(name = '#' + str(i), target = Query)
    -    threadArray.append(thread)
    -    thread.start()
    -
    -for t in threadArray:
    -    t.join()
    -
    -print("All done!")
    -
    - -

    The script logic does not need to be changed to benefit from - DRCP connection pooling.

    - -

    Run the script:

    - -
    python connect_pool2.py
    - -

    Review drcp_query.sql and set the connection string to - your database. Then open a new a terminal window and invoke SQL*Plus:

    - -
    sqlplus /nolog @drcp_query.sql
    - -

    This will prompt for the SYSTEM password and the database connection - string. With Pluggable databases, you will need to connect to the - container database. Note that with ADB, this view does not contain - rows, so running this script is not useful.

    - -

    For other databases, the script shows the number of connection requests - made to the pool since the database was started ("NUM_REQUESTS"), how many - of those reused a pooled server's session ("NUM_HITS"), and how many had - to create new sessions ("NUM_MISSES"). Typically the goal is a low number - of misses.

    - -

    To see the pool configuration you can query DBA_CPOOL_INFO.

    -
  • - -
  • -

    2.5 More DRCP investigation

    - -

    To explore the behaviors of cx_Oracle connection pooling and DRCP pooling futher, - you could try changing the purity to - cx_Oracle.ATTR_PURITY_NEW to see the effect on the - DRCP NUM_MISSES statistic.

    - -

    Another experiement is to include the time module at the file - top:

    - -
    -import time
    - -

    and add calls to time.sleep(1) in the code, for - example in the query loop. Then look at the way the threads execute. Use - drcp_query.sql to monitor the pool's behavior.

    - - -
  • -
- -

3. Fetching Data

- - -

Executing SELECT queries is the primary way to get data from Oracle - Database. Documentation link for further reading: SQL Queries.

- -
    -
  • 3.1 A simple query

    - -

    There are a number of functions you can use to query an Oracle - database, but the basics of querying are always the same:

    - -

    1. Execute the statement.
    - 2. Bind data values (optional).
    - 3. Fetch the results from the database.

    - -

    Review the code contained in query2.py:

    - -
    -import cx_Oracle
    -import db_config
    -
    -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn)
    -
    -cur = con.cursor()
    -cur.execute("select * from dept order by deptno")
    -for deptno, dname, loc in cur:
    -    print("Department number: ", deptno)
    -    print("Department name: ", dname)
    -    print("Department location:", loc)
    -
    - -

    The cursor() method opens a cursor for statements to use.

    - -

    The execute() method parses and executes the statement.

    - -

    The loop fetches each row from the cursor and unpacks the returned - tuple into the variables deptno, dname, - loc, which are then printed.

    - -

    Run the script in a terminal window:

    - -
    python query2.py
    - -

    The output is:

    - -
    Department number:  10
    -Department name:  ACCOUNTING
    -Department location: NEW YORK
    -Department number:  20
    -Department name:  RESEARCH
    -Department location: DALLAS
    -Department number:  30
    -Department name:  SALES
    -Department location: CHICAGO
    -Department number:  40
    -Department name:  OPERATIONS
    -Department location: BOSTON
    - -
  • - -
  • 3.2 Using fetchone()

    - -

    When the number of rows is large, the fetchall() - call may use too much memory.

    - -

    Review the code contained in query_one.py:

    - -
    -import cx_Oracle
    -import db_config
    -
    -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn)
    -cur = con.cursor()
    -
    -cur.execute("select * from dept order by deptno")
    -row = cur.fetchone()
    -print(row)
    -
    -row = cur.fetchone()
    -print(row)
    -
    - -

    This uses the fetchone() method to return just a single row as a - tuple. When called multiple time, consecutive rows are returned:

    - -

    Run the script in a terminal window:

    - -
    python query_one.py
    - -

    The first two rows of the table are printed.

    - -
  • - -
  • 3.3 Using fetchmany()

    - -

    Review the code contained in query_many.py:

    - -
    -import cx_Oracle
    -import db_config
    -
    -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn)
    -cur = con.cursor()
    -
    -cur.execute("select * from dept order by deptno")
    -res = cur.fetchmany(numRows = 3)
    -print(res)
    -
    - -

    The fetchmany() method returns a list of tuples. By - default the number of rows returned is specified by the cursor - attribute arraysize (which defaults to 100). Here the - numRows parameter specifies that three rows should be - returned.

    - -

    Run the script in a terminal window:

    - -
    python query_many.py
    - -

    The first three rows of the table are returned as a list - (Python's name for an array) of tuples.

    - -

    You can access elements of the lists by position indexes. To see this, - edit the file and add:

    - -
    -print(res[0])    # first row
    -print(res[0][1]) # second element of first row
    -
    - -
  • - -
  • 3.4 Scrollable cursors

    - -

    Scrollable cursors enable the application to move backwards as - well as forwards in query results. They can be used to skip rows - as well as move to a particular row.

    - -

    Review the code contained in query_scroll.py:

    - -
    -import cx_Oracle
    -import db_config
    -
    -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn)
    -cur = con.cursor(scrollable = True)
    -
    -cur.execute("select * from dept order by deptno")
    -
    -cur.scroll(2, mode = "absolute")  # go to second row
    -print(cur.fetchone())
    -
    -cur.scroll(-1)                    # go back one row
    -print(cur.fetchone())
    -
    - -

    Run the script in a terminal window:

    - -
    python query_scroll.py
    - -

    Edit query_scroll.py and experiment with different - scroll options and orders, such as:

    - -
    cur.scroll(1)  # go to next row
    -print(cur.fetchone())
    -
    -cur.scroll(mode = "first")  # go to first row
    -print(cur.fetchone())
    -
    - -

    Try some scroll options that go beyond the number of rows in - the resultset.

    - -
  • - -
  • 3.5 Tuning with arraysize and prefetchrows

    - -

    This section demonstrates a way to improve query performance by increasing - the number of rows returned in each batch from Oracle to the Python - program.

    - -

    Row prefetching and array fetching are both internal buffering techniques - to reduce round-trips to the database. The difference is the code layer that - is doing the buffering, and when the buffering occurs.

    - -

    First, create a table with a large number of rows. - Review query_arraysize.sql:

    - -
    -create table bigtab (mycol varchar2(20));
    -begin
    -  for i in 1..20000
    -  loop
    -   insert into bigtab (mycol) values (dbms_random.string('A',20));
    -  end loop;
    -end;
    -/
    -show errors
    -
    -commit;
    -
    - -

    In a terminal window run the script as:

    - -
    sqlplus /nolog @query_arraysize.sql
    - -

    Review the code contained in query_arraysize.py:

    - -
    -import cx_Oracle
    -import time
    -import db_config
    -
    -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn)
    -
    -start = time.time()
    -
    -cur = con.cursor()
    -cur.prefetchrows = 100
    -cur.arraysize = 100
    -cur.execute("select * from bigtab")
    -res = cur.fetchall()
    -# print(res)  # uncomment to display the query results
    -
    -elapsed = (time.time() - start)
    -print(elapsed, "seconds")
    -
    - -

    This uses the 'time' module to measure elapsed time of the query. The - prefetchrows and arraysize values are 100. This causes batches of 100 - records at a time to be returned from the database to a cache in Python. - These values can be tuned to reduce the number of "round-trips" - made to the database, often reducing network load and reducing the number of - context switches on the database server. The fetchone(), - fetchmany() and fetchall() methods will read from - the cache before requesting more data from the database.

    - -

    In a terminal window, run:

    - -
    python query_arraysize.py
    - -

    Rerun a few times to see the average times.

    - -

    Experiment with different prefetchrows and arraysize values. For - example, edit query_arraysize.py and change the arraysize - to:

    - -
    cur.arraysize = 2000
    - -

    Rerun the script to compare the performance of different - arraysize settings.

    - -

    In general, larger array sizes improve performance. Depending on how - fast your system is, you may need to use different values than those - given here to see a meaningful time difference.

    - -

    There is a time/space tradeoff for increasing the values. Larger values - will require more memory in Python for buffering the records.

    - -

    If you know the query returns a fixed number of rows, for example 20 - rows, then set arraysize to 20 and prefetchrows to 21. The addition of one - for prefetchrows prevents a round-trip to check for end-of-fetch. The - statement execution and fetch will take a total of one round-trip. This - minimizes load on the database.

    - -

    If you know a query only returns a few records, - decrease the arraysize from the default to reduce memory - usage.

    -
  • -
- -

4. Binding Data

- -

Bind variables enable you to re-execute statements with new data values - without the overhead of re-parsing the statement. Binding improves code - reusability, improves application scalability, and can reduce the risk of SQL - injection attacks. Using bind variables is strongly recommended. - Documentation link for further reading: Using - Bind Variables.

- -
    - -
  • 4.1 Binding in queries

    - -

    Review the code contained in bind_query.py:

    - -
    -import cx_Oracle
    -import db_config
    -
    -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn)
    -cur = con.cursor()
    -
    -sql = "select * from dept where deptno = :id order by deptno"
    -
    -cur.execute(sql, id = 20)
    -res = cur.fetchall()
    -print(res)
    -
    -cur.execute(sql, id = 10)
    -res = cur.fetchall()
    -print(res)
    -
    - -

    The statement contains a bind variable ":id" placeholder. - The statement is executed twice with different values for the - WHERE clause.

    - -

    From a terminal window, run:

    - -
    python bind_query.py
    - -

    The output shows the details for the two departments.

    - -

    An arbitrary number of named arguments can be used in an - execute() call. Each argument name must match a bind - variable name. Alternatively, instead of passing multiple arguments you - could pass a second argument to execute() that is a sequence - or a dictionary. Later examples show these syntaxes.

    - -

    To bind a database NULL, use the Python value None

    - -

    cx_Oracle uses Oracle Database's Statement Cache. As long as the - statement you pass to execute() is in that cache, you can use - different bind values and still avoid a full statement parse. The - statement cache size is configurable for each connection. To see the - default statement cache size, edit bind_query.py and add a - line at the end:

    - -
    -print(con.stmtcachesize)
    -
    - -

    Re-run the file.

    - -

    In your applications you would set the statement cache size to the - number of unique statements commonly executed.

    - -
  • - -
  • 4.2 Binding in inserts

    - -

    Review the code in bind_insert.sql creating a table - for inserting data:

    - -
    -create table mytab (id number, data varchar2(20), constraint my_pk primary key (id));
    -
    - -

    Run the script as:

    - -
    sqlplus /nolog @bind_insert.sql
    - -

    Review the code contained in bind_insert.py:

    - -
    -import cx_Oracle
    -import db_config
    -
    -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn)
    -cur = con.cursor()
    -
    -rows = [ (1, "First" ), (2, "Second" ),
    -         (3, "Third" ), (4, "Fourth" ),
    -         (5, "Fifth" ), (6, "Sixth" ),
    -         (7, "Seventh" ) ]
    -
    -cur.executemany("insert into mytab(id, data) values (:1, :2)", rows)
    -
    -# Now query the results back
    -
    -cur2 = con.cursor()
    -cur2.execute('select * from mytab')
    -res = cur2.fetchall()
    -print(res)
    -
    - -

    The 'rows' array contains the data to be inserted.

    - -

    The executemany() call inserts all rows. This - call uses "array binding", which is an efficient way to - insert multiple records.

    - -

    The final part of the script queries the results back and displays them as a list of tuples.

    - -

    From a terminal window, run:

    - -
    python bind_insert.py
    - -

    The new results are automatically rolled back at the end of - the script so re-running it will always show the same number of - rows in the table.

    - -
  • - -
  • 4.3 Batcherrors

    - -

    The Batcherrors features allows invalid data to be identified - while allowing valid data to be inserted.

    - -

    Edit the data values in bind_insert.py and - create a row with a duplicate key:

    - -
    -rows = [ (1, "First" ), (2, "Second" ),
    -         (3, "Third" ), (4, "Fourth" ),
    -         (5, "Fifth" ), (6, "Sixth" ),
    -         (6, "Duplicate" ),
    -         (7, "Seventh" ) ]
    -
    -
    - -

    From a terminal window, run:

    - -
    python bind_insert.py
    - -

    The duplicate generates the error "ORA-00001: unique - constraint (PYTHONHOL.MY_PK) violated". The data is rolled back - and the query returns no rows.

    - -

    Edit the file again and enable batcherrors like:

    - -
    -import cx_Oracle
    -import db_config
    -
    -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn)
    -cur = con.cursor()
    -
    -rows = [ (1, "First" ), (2, "Second" ),
    -         (3, "Third" ), (4, "Fourth" ),
    -         (5, "Fifth" ), (6, "Sixth" ),
    -         (6, "Duplicate" ),
    -         (7, "Seventh" ) ]
    -
    -cur.executemany("insert into mytab(id, data) values (:1, :2)", rows, batcherrors = True)
    -
    -for error in cur.getbatcherrors():
    -    print("Error", error.message.rstrip(), "at row offset", error.offset)
    -
    -# Now query the results back
    -
    -cur2 = con.cursor()
    -cur2.execute('select * from mytab')
    -res = cur2.fetchall()
    -print(res)
    -
    - -

    Run the file:

    - -
    python bind_insert.py
    - -

    The new code shows the offending duplicate row: "ORA-00001: - unique constraint (PYTHONHOL.MY_PK) violated at row offset 6". - This indicates the 6th data value (counting from 0) had a - problem.

    - -

    The other data gets inserted and is queried back.

    - -

    At the end of the script, cx_Oracle will roll back an uncommitted transaction. If you want to commit results, you can use:

    - -
    con.commit()
    - -

    To force cx_Oracle to roll back, use:

    - -
    con.rollback()
    - -
  • - -
  • 4.4 Binding named objects

    - -

    cx_Oracle can fetch and bind named object types such as Oracle's - Spatial Data Objects (SDO).

    - -

    In a terminal window, start SQL*Plus using the lab credentials and connection string, such as:

    - -
    -sqlplus -l pythonhol/welcome@localhost/orclpdb1
    -
    - -

    Use the SQL*Plus DESCRIBE command to look at the SDO definition:

    - -
    -desc MDSYS.SDO_GEOMETRY
    -
    - -

    It contains various attributes and methods. The top level description is:

    - -
    - Name                                      Null?    Type
    - ----------------------------------------- -------- ----------------------------
    - SDO_GTYPE                                          NUMBER
    - SDO_SRID                                           NUMBER
    - SDO_POINT                                          MDSYS.SDO_POINT_TYPE
    - SDO_ELEM_INFO                                      MDSYS.SDO_ELEM_INFO_ARRAY
    - SDO_ORDINATES                                      MDSYS.SDO_ORDINATE_ARRAY
    -
    - -

    Review the code contained in bind_sdo.py:

    - -
    -import cx_Oracle
    -import db_config
    -
    -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn)
    -cur = con.cursor()
    -
    -# Create table
    -cur.execute("""begin
    -                 execute immediate 'drop table testgeometry';
    -                 exception when others then
    -                   if sqlcode <> -942 then
    -                     raise;
    -                   end if;
    -               end;""")
    -cur.execute("""create table testgeometry (
    -               id number(9) not null,
    -               geometry MDSYS.SDO_GEOMETRY not null)""")
    -
    -# Create and populate Oracle objects
    -typeObj = con.gettype("MDSYS.SDO_GEOMETRY")
    -elementInfoTypeObj = con.gettype("MDSYS.SDO_ELEM_INFO_ARRAY")
    -ordinateTypeObj = con.gettype("MDSYS.SDO_ORDINATE_ARRAY")
    -obj = typeObj.newobject()
    -obj.SDO_GTYPE = 2003
    -obj.SDO_ELEM_INFO = elementInfoTypeObj.newobject()
    -obj.SDO_ELEM_INFO.extend([1, 1003, 3])
    -obj.SDO_ORDINATES = ordinateTypeObj.newobject()
    -obj.SDO_ORDINATES.extend([1, 1, 5, 7])
    -print("Created object", obj)
    -
    -# Add a new row
    -print("Adding row to table...")
    -cur.execute("insert into testgeometry values (1, :objbv)", objbv = obj)
    -print("Row added!")
    -
    -# Query the row
    -print("Querying row just inserted...")
    -cur.execute("select id, geometry from testgeometry");
    -for row in cur:
    -    print(row)
    -
    - -

    This uses gettype() to get the database types of the -SDO and its object attributes. The newobject() calls -create Python representations of those objects. The python object -atributes are then set. Oracle VARRAY types such as -SDO_ELEM_INFO_ARRAY are set with extend().

    - -

    Run the file:

    - -
    python bind_sdo.py
    - -

    The new SDO is shown as an object, similar to:

    - -
    (1, <cx_Oracle.Object MDSYS.SDO_GEOMETRY at 0x104a76230>)
    - -

    To show the attribute values, edit the the query code section at -the end of the file. Add a new method that traverses the object. The -file below the existing comment "# (Change below here)") -should look like:

    - -
    -# (Change below here)
    -
    -# Define a function to dump the contents of an Oracle object
    -def dumpobject(obj, prefix = "  "):
    -    if obj.type.iscollection:
    -        print(prefix, "[")
    -        for value in obj.aslist():
    -            if isinstance(value, cx_Oracle.Object):
    -                dumpobject(value, prefix + "  ")
    -            else:
    -                print(prefix + "  ", repr(value))
    -        print(prefix, "]")
    -    else:
    -        print(prefix, "{")
    -        for attr in obj.type.attributes:
    -            value = getattr(obj, attr.name)
    -            if isinstance(value, cx_Oracle.Object):
    -                print(prefix + "  " + attr.name + " :")
    -                dumpobject(value, prefix + "    ")
    -            else:
    -                print(prefix + "  " + attr.name + " :", repr(value))
    -        print(prefix, "}")
    -
    -# Query the row
    -print("Querying row just inserted...")
    -cur.execute("select id, geometry from testgeometry")
    -for id, obj in cur:
    -    print("Id: ", id)
    -    dumpobject(obj)
    -
    - -

    Run the file again:

    - -
    python bind_sdo.py
    - -

    This shows

    -
    -Querying row just inserted...
    -Id:  1
    -   {
    -    SDO_GTYPE : 2003
    -    SDO_SRID : None
    -    SDO_POINT : None
    -    SDO_ELEM_INFO :
    -       [
    -         1
    -         1003
    -         3
    -       ]
    -    SDO_ORDINATES :
    -       [
    -         1
    -         1
    -         5
    -         7
    -       ]
    -   }
    -
    - -

    To explore further, try setting the SDO attribute SDO_POINT, which -is of type SDO_POINT_TYPE.

    - -

    The gettype() and newobject() methods can -also be used to bind PL/SQL Records and Collections.

    - -

    Before deciding to use objects, review your performance goals because -working with scalar values can be faster.

    - -
  • -
- -

5. PL/SQL

- -

PL/SQL is Oracle's procedural language extension to SQL. PL/SQL - procedures and functions are stored and run in the database. Using - PL/SQL lets all database applications reuse logic, no matter how the - application accesses the database. Many data-related operations can - be performed in PL/SQL faster than extracting the data into a - program (for example, Python) and then processing it. Documentation link - for further reading: PL/SQL Execution.

- -
    -
  • 5.1 PL/SQL functions

    - -

    Review plsql_func.sql which creates a PL/SQL - stored function myfunc() to insert a row into a new - table named ptab and return double the inserted - value:

    - -
    -create table ptab (mydata varchar(20), myid number);
    -
    -create or replace function myfunc(d_p in varchar2, i_p in number) return number as
    -  begin
    -    insert into ptab (mydata, myid) values (d_p, i_p);
    -    return (i_p * 2);
    -  end;
    -/
    -
    - -

    Run the script using:

    - -
    sqlplus /nolog @plsql_func.sql
    - -

    Review the code contained in plsql_func.py:

    - -
    -import cx_Oracle
    -import db_config
    -
    -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn)
    -cur = con.cursor()
    -
    -res = cur.callfunc('myfunc', int, ('abc', 2))
    -print(res)
    -
    - -

    This uses callfunc() to execute the function. - The second parameter is the type of the returned value. It should be one - of the types supported by cx_Oracle or one of the type constants defined - by cx_Oracle (such as cx_Oracle.NUMBER). The two PL/SQL function - parameters are passed as a tuple, binding them to the function parameter - arguments.

    - -

    From a terminal window, run:

    - -
    python plsql_func.py
    - -

    The output is a result of the PL/SQL function calculation.

    - -
  • - -
  • 5.2 PL/SQL procedures

    - -

    Review plsql_proc.sql which creates a PL/SQL procedure -myproc() to accept two parameters. The second parameter -contains an OUT return value.

    - -
    -create or replace procedure myproc(v1_p in number, v2_p out number) as
    -begin
    -  v2_p := v1_p * 2;
    -end;
    -/
    -
    - -

    Run the script with:

    - -
    sqlplus /nolog @plsql_proc.sql
    - -

    Review the code contained in plsql_proc.py:

    - -
    -import cx_Oracle
    -import db_config
    -
    -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn)
    -cur = con.cursor()
    -
    -myvar = cur.var(int)
    -cur.callproc('myproc', (123, myvar))
    -print(myvar.getvalue())
    -
    - -

    This creates an integer variable myvar to hold - the value returned by the PL/SQL OUT parameter. The input number - 123 and the output variable name are bound to the procedure call - parameters using a tuple.

    - -

    To call the PL/SQL procedure, the callproc() - method is used.

    - -

    In a terminal window, run:

    - -
    python plsql_proc.py
    - -

    The getvalue() method displays the returned - value.

    -
  • -
- -

6. Type Handlers

- -

Type handlers enable applications to alter data that is fetched from, or sent -to, the database. Documentation links for further reading: Changing Fetched Data Types with Output Type Handlers and Changing Bind Data Types using an Input Type Handler.

- -
    -
  • -

    6.1 Basic output type handler

    - -

    Output type handlers enable applications to change how data - is fetched from the database. For example, numbers can be - returned as strings or decimal objects. LOBs can be returned as - string or bytes.

    - -

    A type handler is enabled by setting the - outputtypehandler attribute on either a cursor or - the connection. If set on a cursor it only affects queries executed - by that cursor. If set on a connection it affects all queries executed - on cursors created by that connection.

    - -

    Review the code contained in type_output.py:

    - -
    -import cx_Oracle
    -import db_config
    -
    -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn)
    -cur = con.cursor()
    -
    -print("Standard output...")
    -for row in cur.execute("select * from dept"):
    -    print(row)
    -
    - -

    In a terminal window, run:

    - -
    python type_output.py
    - -

    This shows the department number represented as digits like - 10.

    - -

    Add an output type handler to the bottom of the file:

    - -
    -def ReturnNumbersAsStrings(cursor, name, defaultType, size, precision, scale):
    -    if defaultType == cx_Oracle.NUMBER:
    -        return cursor.var(str, 9, cursor.arraysize)
    -
    -print("Output type handler output...")
    -cur = con.cursor()
    -cur.outputtypehandler = ReturnNumbersAsStrings
    -for row in cur.execute("select * from dept"):
    -    print(row)
    -
    - -

    This type handler converts any number columns to strings with - maxium size 9.

    - -

    Run the script again:

    - -
    python type_output.py
    - -

    The new output shows the department numbers are now strings - within quotes like '10'.

    - -
  • - -
  • 6.2 Output type handlers and variable converters

    - -

    When numbers are fetched from the database, the conversion - from Oracle's decimal representation to Python's binary format - may need careful handling. To avoid unexpected issues, the - general recommendation is to do number operations in SQL or - PL/SQL, or to use the decimal module in Python.

    - -

    Output type handlers can be combined with variable converters - to change how data is fetched.

    - -

    Review type_converter.py:

    - -
    -import cx_Oracle
    -import db_config
    -
    -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn)
    -cur = con.cursor()
    -
    -for value, in cur.execute("select 0.1 from dual"):
    -    print("Value:", value, "* 3 =", value * 3)
    -
    - -

    Run the file:

    - -
    python type_converter.py
    - -

    The output is like:

    - -
    Value: 0.1 * 3 = 0.30000000000000004
    - -

    Edit the file and add a type handler that uses a Python - decimal converter:

    - -
    -import cx_Oracle
    -import decimal
    -import db_config
    -
    -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn)
    -cur = con.cursor()
    -
    -def ReturnNumbersAsDecimal(cursor, name, defaultType, size, precision, scale):
    -    if defaultType == cx_Oracle.NUMBER:
    -        return cursor.var(str, 9, cursor.arraysize, outconverter = decimal.Decimal)
    -
    -cur.outputtypehandler = ReturnNumbersAsDecimal
    -
    -for value, in cur.execute("select 0.1 from dual"):
    -    print("Value:", value, "* 3 =", value * 3)
    -
    - -

    The Python decimal.Decimal converter gets called - with the string representation of the Oracle number. The output - from decimal.Decimal is returned in the output - tuple.

    - -

    Run the file again:

    - -
    python type_converter.py
    - -

    Output is like:

    - -
    Value: 0.1 * 3 = 0.3
    - -

    Although the code demonstrates the use of outconverter, in this - particular case, the variable can be created simply by using the - following code to replace the outputtypehandler function defined - above:

    - -
    -def ReturnNumbersAsDecimal(cursor, name, defaultType, size, precision, scale):
    -    if defaultType == cx_Oracle.NUMBER:
    -        return cursor.var(decimal.Decimal, arraysize = cursor.arraysize)
    -
    - -
  • - -
  • 6.3 Input type handlers

    - -

    Input type handlers enable applications to change how data is - bound to statements, or to enable new types to be bound directly - without having to be converted individually.

    - -

    Review type_input.py, which is similar to the - final bind_sdo.py from section 4.4, with the - addition of a new class and converter (shown in bold):

    - -
    -import cx_Oracle
    -import db_config
    -
    -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn)
    -cur = con.cursor()
    -
    -# Create table
    -cur.execute("""begin
    -                 execute immediate 'drop table testgeometry';
    -                 exception when others then
    -                   if sqlcode <> -942 then
    -                     raise;
    -                   end if;
    -               end;""")
    -cur.execute("""create table testgeometry (
    -               id number(9) not null,
    -               geometry MDSYS.SDO_GEOMETRY not null)""")
    -
    -# Create a Python class for an SDO
    -class mySDO(object):
    -
    -    def __init__(self, gtype, elemInfo, ordinates):
    -        self.gtype = gtype
    -        self.elemInfo = elemInfo
    -        self.ordinates = ordinates
    -
    -# Get Oracle type information
    -objType = con.gettype("MDSYS.SDO_GEOMETRY")
    -elementInfoTypeObj = con.gettype("MDSYS.SDO_ELEM_INFO_ARRAY")
    -ordinateTypeObj = con.gettype("MDSYS.SDO_ORDINATE_ARRAY")
    -
    -# Convert a Python object to MDSYS.SDO_GEOMETRY
    -def SDOInConverter(value):
    -    obj = objType.newobject()
    -    obj.SDO_GTYPE = value.gtype
    -    obj.SDO_ELEM_INFO = elementInfoTypeObj.newobject()
    -    obj.SDO_ELEM_INFO.extend(value.elemInfo)
    -    obj.SDO_ORDINATES = ordinateTypeObj.newobject()
    -    obj.SDO_ORDINATES.extend(value.ordinates)
    -    return obj
    -
    -def SDOInputTypeHandler(cursor, value, numElements):
    -    if isinstance(value, mySDO):
    -        return cursor.var(cx_Oracle.OBJECT, arraysize = numElements,
    -                inconverter = SDOInConverter, typename = objType.name)
    -
    -sdo = mySDO(2003, [1, 1003, 3], [1, 1, 5, 7])  # Python object
    -cur.inputtypehandler = SDOInputTypeHandler
    -cur.execute("insert into testgeometry values (:1, :2)", (1, sdo))
    -
    -# Define a function to dump the contents of an Oracle object
    -def dumpobject(obj, prefix = "  "):
    -    if obj.type.iscollection:
    -        print(prefix, "[")
    -        for value in obj.aslist():
    -            if isinstance(value, cx_Oracle.Object):
    -                dumpobject(value, prefix + "  ")
    -            else:
    -                print(prefix + "  ", repr(value))
    -        print(prefix, "]")
    -    else:
    -        print(prefix, "{")
    -        for attr in obj.type.attributes:
    -            value = getattr(obj, attr.name)
    -            if isinstance(value, cx_Oracle.Object):
    -                print(prefix + "  " + attr.name + " :")
    -                dumpobject(value, prefix + "    ")
    -            else:
    -                print(prefix + "  " + attr.name + " :", repr(value))
    -        print(prefix, "}")
    -
    -# Query the row
    -print("Querying row just inserted...")
    -cur.execute("select id, geometry from testgeometry")
    -for (id, obj) in cur:
    -    print("Id: ", id)
    -    dumpobject(obj)
    -
    - -

    In the new file, a Python class mySDO is defined, -which has attributes corresponding to each Oracle MDSYS.SDO_GEOMETRY -attribute. - -The mySDO class is used lower in the code to create a -Python instance:

    - -
    -sdo = mySDO(2003, [1, 1003, 3], [1, 1, 5, 7])
    - -

    which is then directly bound into the INSERT statement like:

    - -
    cur.execute("insert into testgeometry values (:1, :2)", (1, sdo))
    - -

    The mapping between Python and Oracle objects is handled in -SDOInConverter which uses the cx_Oracle -newobject() and extend() methods to create -an Oracle object from the Python object values. The -SDOInConverter method is called by the input type handler -SDOInputTypeHandler whenever an instance of -mySDO is inserted with the cursor.

    - -

    To confirm the behavior, run the file:

    - -
    python type_input.py
    - -
  • - -
- -

7. LOBs

- -

Oracle Database "LOB" long objects can be streamed using a LOB - locator, or worked with directly as strings or bytes. Documentation link - for further reading: Using CLOB and BLOB Data.

- -
    -
  • -

    7.1 Fetching a CLOB using a locator

    - -

    Review the code contained in clob.py:

    - -
    -import cx_Oracle
    -import db_config
    -
    -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn)
    -cur = con.cursor()
    -
    -print("Inserting data...")
    -cur.execute("truncate table testclobs")
    -longString = ""
    -for i in range(5):
    -    char = chr(ord('A') + i)
    -    longString += char * 250
    -    cur.execute("insert into testclobs values (:1, :2)",
    -                   (i + 1, "String data " + longString + ' End of string'))
    -con.commit()
    -
    -print("Querying data...")
    -cur.execute("select * from testclobs where id = :id", {'id': 1})
    -(id, clob) = cur.fetchone()
    -print("CLOB length:", clob.size())
    -clobdata = clob.read()
    -print("CLOB data:", clobdata)
    -
    - -

    This inserts some test string data and then fetches one - record into clob, which is a cx_Oracle character - LOB Object. Methods on LOB include size() and - read().

    - -

    To see the output, run the file:

    - -
    python clob.py
    - -

    Edit the file and experiment reading chunks of data by giving - start character position and length, such as - clob.read(1,10)

    - -
  • - -
  • -

    7.2 Fetching a CLOB as a string

    - -

    For CLOBs small enough to fit in the application memory, it - is much faster to fetch them directly as strings.

    - -

    Review the code contained in clob_string.py. - The differences from clob.py are shown in bold:

    - -
    -import cx_Oracle
    -import db_config
    -
    -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn)
    -cur = con.cursor()
    -
    -print("Inserting data...")
    -cur.execute("truncate table testclobs")
    -longString = ""
    -for i in range(5):
    -    char = chr(ord('A') + i)
    -    longString += char * 250
    -    cur.execute("insert into testclobs values (:1, :2)",
    -                (i + 1, "String data " + longString + ' End of string'))
    -con.commit()
    -
    -def OutputTypeHandler(cursor, name, defaultType, size, precision, scale):
    -    if defaultType == cx_Oracle.CLOB:
    -        return cursor.var(cx_Oracle.LONG_STRING, arraysize = cursor.arraysize)
    -
    -con.outputtypehandler = OutputTypeHandler
    -
    -print("Querying data...")
    -cur.execute("select * from testclobs where id = :id", {'id': 1})
    -(id, clobdata) = cur.fetchone()
    -print("CLOB length:", len(clobdata))
    -print("CLOB data:", clobdata)
    -
    - -

    The OutputTypeHandler causes cx_Oracle to fetch the CLOB as a - string. Standard Python string functions such as - len() can be used on the result.

    - -

    The output is the same as for clob.py. To - check, run the file:

    - -
    python clob_string.py
    - -
  • -
- -

8. Rowfactory functions

- -

Rowfactory functions enable queries to return objects other than - tuples. They can be used to provide names for the various columns - or to return custom objects.

- -
    -
  • 8.1 Rowfactory for mapping column names

    - -

    Review the code contained in rowfactory.py:

    - -
    -import collections
    -import cx_Oracle
    -import db_config
    -
    -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn)
    -cur = con.cursor()
    -
    -cur.execute("select deptno, dname from dept")
    -rows = cur.fetchall()
    -
    -print('Array indexes:')
    -for row in rows:
    -    print(row[0], "->", row[1])
    -
    -print('Loop target variables:')
    -for c1, c2 in rows:
    -    print(c1, "->", c2)
    -
    - -

    This shows two methods of accessing result set items from a data - row. The first uses array indexes like row[0]. The - second uses loop target variables which take the values of each row - tuple.

    - -

    Run the file:

    - -
    python rowfactory.py
    - -

    Both access methods gives the same results.

    - -

    To use a rowfactory function, edit rowfactory.py and - add this code at the bottom:

    - -
    -print('Rowfactory:')
    -cur.execute("select deptno, dname from dept")
    -cur.rowfactory = collections.namedtuple("MyClass", ["DeptNumber", "DeptName"])
    -
    -rows = cur.fetchall()
    -for row in rows:
    -    print(row.DeptNumber, "->", row.DeptName)
    -
    - -

    This uses the Python factory function - namedtuple() to create a subclass of tuple that allows - access to the elements via indexes or the given field names.

    - -

    The print() function shows the use of the new - named tuple fields. This coding style can help reduce coding - errors.

    - -

    Run the script again:

    - -
    python rowfactory.py
    - - -

    The output results are the same.

    - -
  • -
- -

9. Subclassing connections and cursors

- -

Subclassing enables application to "hook" connection and cursor - creation. This can be used to alter or log connection and execution - parameters, and to extend cx_Oracle functionality. Documentation link for - further reading: Tracing SQL and PL/SQL Statements.

- -
    -
  • 9.1 Subclassing connections

    - -

    Review the code contained in subclass.py:

    - -
    -import cx_Oracle
    -import db_config
    -
    -class MyConnection(cx_Oracle.Connection):
    -
    -    def __init__(self):
    -        print("Connecting to database")
    -        return super(MyConnection, self).__init__(db_config.user, db_config.pw, db_config.dsn)
    -
    -con = MyConnection()
    -cur = con.cursor()
    -
    -cur.execute("select count(*) from emp where deptno = :bv", (10,))
    -count, = cur.fetchone()
    -print("Number of rows:", count)
    -
    - -

    This creates a new class "MyConnection" that inherits from the - cx_Oracle Connection class. The __init__ method is - invoked when an instance of the new class is created. It prints a - message and calls the base class, passing the connection - credentials.

    - -

    In the "normal" application, the application code:

    - -
    con = MyConnection()
    - -

    does not need to supply any credentials, as they are embedded in the - custom subclass. All the cx_Oracle methods such as cursor() are - available, as shown by the query.

    - -

    Run the file:

    - -
    python subclass.py
    - -

    The query executes successfully.

    - -
  • - -
  • 9.2 Subclassing cursors

    - -

    Edit subclass.py and extend the - cursor() method with a new MyCursor class:

    - -
    -import cx_Oracle
    -import db_config
    -
    -class MyConnection(cx_Oracle.Connection):
    -
    -    def __init__(self):
    -        print("Connecting to database")
    -        return super(MyConnection, self).__init__(db_config.user, db_config.pw, db_config.dsn)
    -
    -    def cursor(self):
    -        return MyCursor(self)
    -
    -class MyCursor(cx_Oracle.Cursor):
    -
    -   def execute(self, statement, args):
    -       print("Executing:", statement)
    -       print("Arguments:")
    -       for argIndex, arg in enumerate(args):
    -           print("  Bind", argIndex + 1, "has value", repr(arg))
    -           return super(MyCursor, self).execute(statement, args)
    -
    -   def fetchone(self):
    -       print("Fetchone()")
    -       return super(MyCursor, self).fetchone()
    -
    -con = MyConnection()
    -cur = con.cursor()
    -
    -cur.execute("select count(*) from emp where deptno = :bv", (10,))
    -count, = cur.fetchone()
    -print("Number of rows:", count)
    -
    - -

    When the application gets a cursor from the -MyConnection class, the new cursor() method -returns an instance of our new MyCursor class.

    - -

    The "application" query code remains unchanged. The new -execute() and fetchone() methods of the -MyCursor class get invoked. They do some logging and -invoke the parent methods to do the actual statement execution.

    - -

    To confirm this, run the file again:

    - -
    python subclass.py
    - -
  • - -
- -

10. Advanced Queuing

- -

Oracle Advanced Queuing (AQ) allows messages to be passed between -applications. Documentation link for further reading: Oracle -Advanced Queuing (AQ).

- -
    -
  • 10.1 Message passing with Oracle Advanced Queuing

    - -

    Review aq.py:

    - -
    -import cx_Oracle
    -import decimal
    -import db_config
    -
    -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn)
    -cur = con.cursor()
    -
    -BOOK_TYPE_NAME = "UDT_BOOK"
    -QUEUE_NAME = "BOOKS"
    -QUEUE_TABLE_NAME = "BOOK_QUEUE_TABLE"
    -
    -# Cleanup
    -cur.execute(
    -    """begin
    -         dbms_aqadm.stop_queue('""" + QUEUE_NAME + """');
    -         dbms_aqadm.drop_queue('""" + QUEUE_NAME + """');
    -         dbms_aqadm.drop_queue_table('""" + QUEUE_TABLE_NAME + """');
    -         execute immediate 'drop type """ + BOOK_TYPE_NAME + """';
    -         exception when others then
    -           if sqlcode <> -24010 then
    -             raise;
    -           end if;
    -       end;""")
    -
    -# Create a type
    -print("Creating books type UDT_BOOK...")
    -cur.execute("""
    -        create type %s as object (
    -            title varchar2(100),
    -            authors varchar2(100),
    -            price number(5,2)
    -        );""" % BOOK_TYPE_NAME)
    -
    -# Create queue table and queue and start the queue
    -print("Creating queue table...")
    -cur.callproc("dbms_aqadm.create_queue_table",
    -        (QUEUE_TABLE_NAME, BOOK_TYPE_NAME))
    -cur.callproc("dbms_aqadm.create_queue", (QUEUE_NAME, QUEUE_TABLE_NAME))
    -cur.callproc("dbms_aqadm.start_queue", (QUEUE_NAME,))
    -
    -booksType = con.gettype(BOOK_TYPE_NAME)
    -queue = con.queue(QUEUE_NAME, booksType)
    -
    -# Enqueue a few messages
    -print("Enqueuing messages...")
    -
    -BOOK_DATA = [
    -    ("The Fellowship of the Ring", "Tolkien, J.R.R.", decimal.Decimal("10.99")),
    -    ("Harry Potter and the Philosopher's Stone", "Rowling, J.K.",
    -            decimal.Decimal("7.99"))
    -]
    -
    -for title, authors, price in BOOK_DATA:
    -    book = booksType.newobject()
    -    book.TITLE = title
    -    book.AUTHORS = authors
    -    book.PRICE = price
    -    print(title)
    -    queue.enqOne(con.msgproperties(payload=book))
    -    con.commit()
    -
    -# Dequeue the messages
    -print("\nDequeuing messages...")
    -queue.deqOptions.wait = cx_Oracle.DEQ_NO_WAIT
    -while True:
    -    props = queue.deqOne()
    -    if not props:
    -        break
    -    print(props.payload.TITLE)
    -    con.commit()
    -
    -print("\nDone.")
    -
    - -

    This file sets up Advanced Queuing using Oracle's DBMS_AQADM -package. The queue is used for passing Oracle UDT_BOOK objects. The -file uses AQ interface features enhanced in cx_Oracle 7.2.

    - -

    Run the file:

    - -
    python aq.py
    - -

    The output shows messages being queued and dequeued.

    - -

    To experiment, split the code into three files: one to create and -start the queue, and two other files to queue and dequeue messages. -Experiment running the queue and dequeue files concurrently in -separate terminal windows.

    - -

    Try removing the commit() call in -aq-dequeue.py. Now run aq-enqueue.py once -and then aq-dequeue.py several times. The same messages -will be available each time you try to dequeue them.

    - -

    Change aq-dequeue.py to commit in a separate -transaction by changing the "visibility" setting:

    - -
    -queue.deqOptions.visibility = cx_Oracle.DEQ_IMMEDIATE
    -
    - -

    This gives the same behavior as the original code.

    - -

    Now change the options of enqueued messages so that they expire from the -queue if they have not been dequeued after four seconds:

    - -
    -queue.enqOne(con.msgproperties(payload=book, expiration=4))
    -
    - -

    Now run aq-enqueue.py and wait four seconds before you -run aq-dequeue.py. There should be no messages to -dequeue.

    - -

    If you are stuck, look in the solutions directory at -the aq-dequeue.py, aq-enqueue.py and -aq-queuestart.py files.

    - -
  • -
- -

11. Simple Oracle Document Access (SODA)

- -

Simple Oracle Document Access (SODA) is a set of NoSQL-style APIs. - Documents can be inserted, queried, and retrieved from Oracle - Database. By default, documents are JSON strings. SODA APIs - exist in many languages. Documentation link for further reading: Simple - Oracle Document Access (SODA).

- -
    - -
  • 11.1 Inserting JSON Documents

    - -

    Review soda.py:

    - -
    -import cx_Oracle
    -import db_config
    -
    -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn)
    -
    -soda = con.getSodaDatabase()
    -
    -# Explicit metadata is used for maximum version portability
    -metadata = {
    -               "keyColumn": {
    -                   "name":"ID"
    -               },
    -               "contentColumn": {
    -                   "name": "JSON_DOCUMENT",
    -                   "sqlType": "BLOB"
    -               },
    -               "versionColumn": {
    -                   "name": "VERSION",
    -                   "method": "UUID"
    -               },
    -               "lastModifiedColumn": {
    -                   "name": "LAST_MODIFIED"
    -               },
    -               "creationTimeColumn": {
    -                   "name": "CREATED_ON"
    -               }
    -           }
    -
    -collection = soda.createCollection("friends", metadata)
    -
    -content = {'name': 'Jared', 'age': 35, 'address': {'city': 'Melbourne'}}
    -
    -doc = collection.insertOneAndGet(content)
    -key = doc.key
    -
    -doc = collection.find().key(key).getOne()
    -content = doc.getContent()
    -print('Retrieved SODA document dictionary is:')
    -print(content)
    -
    - -

    soda.createCollection() will create a new collection, or - open an existing collection, if the name is already in use. (Due to a - change in the default "sqlType" storage for Oracle Database 21c, the - metadata is explicitly stated to use a BLOB column. This lets the example - run with different client and database versions).

    - -

    insertOneAndGet() inserts the content of a - document into the database and returns a SODA Document Object. - This allows access to meta data such as the document key. By - default, document keys are automatically generated.

    - -

    The find() method is used to begin an operation - that will act upon documents in the collection.

    - -

    content is a dictionary. You can also get a JSON string - by calling doc.getContentAsString().

    - -

    Run the file:

    - -
    python soda.py
    - -

    The output shows the content of the new document.

    - -
  • - -
  • 11.2 Searching SODA Documents

    - -

    Extend soda.py to insert some more documents and - perform a find filter operation:

    - -
    -myDocs = [
    -    {'name': 'Gerald', 'age': 21, 'address': {'city': 'London'}},
    -    {'name': 'David', 'age': 28, 'address': {'city': 'Melbourne'}},
    -    {'name': 'Shawn', 'age': 20, 'address': {'city': 'San Francisco'}}
    -]
    -collection.insertMany(myDocs)
    -
    -filterSpec = { "address.city": "Melbourne" }
    -myDocuments = collection.find().filter(filterSpec).getDocuments()
    -
    -print('Melbourne people:')
    -for doc in myDocuments:
    -    print(doc.getContent()["name"])
    -
    - -

    Run the script again:

    - -
    python soda.py
    - -

    The find operation filters the collection and returns - documents where the city is Melbourne. Note the - insertMany() method is currently in preview.

    - -

    SODA supports query by example (QBE) with an extensive set of - operators. Extend soda.py with a QBE to find - documents where the age is less than 25:

    - -
    -filterSpec = {'age': {'$lt': 25}}
    -myDocuments = collection.find().filter(filterSpec).getDocuments()
    -
    -print('Young people:')
    -for doc in myDocuments:
    -    print(doc.getContent()["name"])
    -
    - -

    Running the script displays the names.

    - -
  • -
- -

Summary

-

In this tutorial, you have learned how to:

-
    -
  • Create connections
  • -
  • Use cx_Oracle connection pooling and Database Resident Connection Pooling
  • -
  • Execute queries and fetch data
  • -
  • Use bind variables
  • -
  • Use PL/SQL stored functions and procedures
  • -
  • Extend cx_Oracle classes
  • -
  • Use Oracle Advanced Queuing
  • -
  • Use the "SODA" document store API
  • -
- -

For further reading see the cx_Oracle - documentation.

- -

Appendix: Python Primer

- -

Python is a dynamically typed scripting language. It is most - often used to run command-line scripts but is also used for web - applications and web services.

- -

Running Python

- -

You can either:

- -
    - -
  • Create a file of Python commands, such as - myfile.py. This can be run with:

    -
    python myfile.py
  • - -
  • Alternatively run the Python interpreter by executing the - python command in a terminal, and then interactively - enter commands. Use Ctrl-D to exit back to the - operating system prompt.

  • - -
- -

When you run scripts, Python automatically creates bytecode - versions of them in a folder called __pycache__. - These improve performance of scripts that are run multiple times. - They are automatically recreated if the source file changes.

- -

Indentation

- -

Whitespace indentation is significant in Python. When copying - examples, use the same column alignment as shown. The samples in - this lab use spaces, not tabs.

- -

The following indentation prints 'done' once after the loop has - completed:

- -
-for i in range(5):
-    print(i)
-print('done')
-
- -

But this indentation prints 'done' in each iteration:

- -
-for i in range(5):
-    print(i)
-    print('done')
-
- -

Strings

- -

Python strings can be enclosed in - single or double quotes:

- -
'A string constant'
-"another constant"
-

Multi line strings use a triple-quote syntax:

-
"""
-SELECT *
-FROM EMP
-"""
- -

Variables

- -

Variables do not need types declared:

-
count = 1
-ename = 'Arnie'
- -

Comments

- -

Comments are either single line:

-
# a short comment
-

They can be multi-line using the triple-quote token to create a string that does nothing:

-
"""
-a longer
-comment
-"""
-
- -

Printing

- -

Strings and variables can be displayed with a print() function:

-
print('Hello, World!')
-print('Value:', count)
- -

Data Structures

- -

Associative arrays are called 'dictionaries':

-
a2 = {'PI':3.1415, 'E':2.7182}
-

Ordered arrays are called 'lists':

-
a3 = [101, 4, 67]
-

Lists can be accessed via indexes.

-
-print(a3[0])
-print(a3[-1])
-print(a3[1:3])
-
- -

Tuples are like lists but cannot be changed once they are - created. They are created with parentheses:

- -
a4 = (3, 7, 10)
- -

Individual values in a tuple can be assigned to variables like:

- -
v1, v2, v3 = a4
- -

Now the variable v1 contains 3, the variable v2 contains 7 and the variable v3 contains 10.

- -

The value in a single entry tuple like "(13,)"can be - assigned to a variable by putting a comma after the variable name - like:

- -
v1, = (13,)
- -

If the assignment is:

- -
v1 = (13,)
- -

then v1 will contain the whole tuple "(13,)"

- -

Objects

- -

Everything in Python is an object. As an example, given the of the -list a3 above, the append() method can be -used to add a value to the list.

- -
a3.append(23)
-

Now a3 contains [101, 4, 67, 23]

- -

Flow Control

- -

Code flow can be controlled with tests and loops. The -if/elif/else statements look -like:

- -
-if v == 2 or v == 4:
-    print('Even')
-elif v == 1 or v == 3:
-    print('Odd')
-else:
-    print('Unknown number')
-
- -

This also shows how the clauses are delimited with colons, and each -sub block of code is indented.

- -

Loops

- -

A traditional loop is:

-
for i in range(10):
-    print(i)
- -

This prints the numbers from 0 to 9. The value of i - is incremented in each iteration.

- -

The 'for' command can also be used to iterate over - lists and tuples:

- -
-a5 = ['Aa', 'Bb', 'Cc']
-for v in a5:
-    print(v)
-
- -

This sets v to each element of the list -a5 in turn.

- -

Functions

- -

A function may be defined as:

- -
-def myfunc(p1, p2):
-    "Function documentation: add two numbers"
-    print(p1, p2)
-    return p1 + p2
- -

Functions may or may not return values. This function could be called using:

- -
v3 = myfunc(1, 3)
- -

Function calls must appear after their function definition.

- -

Functions are also objects and have attributes. The inbuilt -__doc__ attribute can be used to find the function -description:

- -
print(myfunc.__doc__)
- -

Modules

- -

Sub-files can be included in Python scripts with an import statement.

-
import os
-import sys
-

Many predefined modules exist, such as the os and the sys modules.

- - -

Resources

- - - - - - - - - - -
Copyright © 2017, 2021, Oracle and/or its affiliates. All rights reserved
- - - diff --git a/samples/tutorial/aq.py b/samples/tutorial/aq.py deleted file mode 100644 index df036e6..0000000 --- a/samples/tutorial/aq.py +++ /dev/null @@ -1,80 +0,0 @@ -#------------------------------------------------------------------------------ -# aq.py (Section 10.1) -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -import cx_Oracle -import decimal -import db_config - -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn) -cur = con.cursor() - -BOOK_TYPE_NAME = "UDT_BOOK" -QUEUE_NAME = "BOOKS" -QUEUE_TABLE_NAME = "BOOK_QUEUE_TABLE" - -# Cleanup -cur.execute( - """begin - dbms_aqadm.stop_queue('""" + QUEUE_NAME + """'); - dbms_aqadm.drop_queue('""" + QUEUE_NAME + """'); - dbms_aqadm.drop_queue_table('""" + QUEUE_TABLE_NAME + """'); - execute immediate 'drop type """ + BOOK_TYPE_NAME + """'; - exception when others then - if sqlcode <> -24010 then - raise; - end if; - end;""") - -# Create a type -print("Creating books type UDT_BOOK...") -cur.execute(""" - create type %s as object ( - title varchar2(100), - authors varchar2(100), - price number(5,2) - );""" % BOOK_TYPE_NAME) - -# Create queue table and queue and start the queue -print("Creating queue table...") -cur.callproc("dbms_aqadm.create_queue_table", - (QUEUE_TABLE_NAME, BOOK_TYPE_NAME)) -cur.callproc("dbms_aqadm.create_queue", (QUEUE_NAME, QUEUE_TABLE_NAME)) -cur.callproc("dbms_aqadm.start_queue", (QUEUE_NAME,)) - -booksType = con.gettype(BOOK_TYPE_NAME) -queue = con.queue(QUEUE_NAME, booksType) - -# Enqueue a few messages -print("Enqueuing messages...") - -BOOK_DATA = [ - ("The Fellowship of the Ring", "Tolkien, J.R.R.", decimal.Decimal("10.99")), - ("Harry Potter and the Philosopher's Stone", "Rowling, J.K.", - decimal.Decimal("7.99")) -] - -for title, authors, price in BOOK_DATA: - book = booksType.newobject() - book.TITLE = title - book.AUTHORS = authors - book.PRICE = price - print(title) - queue.enqOne(con.msgproperties(payload=book)) - con.commit() - -# Dequeue the messages -print("\nDequeuing messages...") -queue.deqOptions.wait = cx_Oracle.DEQ_NO_WAIT -while True: - props = queue.deqOne() - if not props: - break - print(props.payload.TITLE) - con.commit() - -print("\nDone.") diff --git a/samples/tutorial/bind_insert.py b/samples/tutorial/bind_insert.py deleted file mode 100644 index f3ec9b4..0000000 --- a/samples/tutorial/bind_insert.py +++ /dev/null @@ -1,27 +0,0 @@ -#------------------------------------------------------------------------------ -# bind_insert.py (Section 4.2 and 4.3) -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -import cx_Oracle -import db_config - -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn) -cur = con.cursor() - -rows = [ (1, "First" ), (2, "Second" ), - (3, "Third" ), (4, "Fourth" ), - (5, "Fifth" ), (6, "Sixth" ), - (7, "Seventh" ) ] - -cur.executemany("insert into mytab(id, data) values (:1, :2)", rows) - -# Now query the results back - -cur2 = con.cursor() -cur2.execute('select * from mytab') -res = cur2.fetchall() -print(res) diff --git a/samples/tutorial/bind_insert.sql b/samples/tutorial/bind_insert.sql deleted file mode 100644 index 5e538f5..0000000 --- a/samples/tutorial/bind_insert.sql +++ /dev/null @@ -1,27 +0,0 @@ -------------------------------------------------------------------------------- --- bind_insert.sql (Section 4.2) -------------------------------------------------------------------------------- - -/*----------------------------------------------------------------------------- - * Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. - *---------------------------------------------------------------------------*/ - -set echo off -@@db_config.sql -set echo on - -connect &user/&pw@&connect_string - -begin - execute immediate 'drop table mytab'; -exception -when others then - if sqlcode not in (-00942) then - raise; - end if; -end; -/ - -create table mytab (id number, data varchar2(20), constraint my_pk primary key (id)); - -exit diff --git a/samples/tutorial/bind_query.py b/samples/tutorial/bind_query.py deleted file mode 100644 index 72d3982..0000000 --- a/samples/tutorial/bind_query.py +++ /dev/null @@ -1,23 +0,0 @@ -#------------------------------------------------------------------------------ -# bind_query.py (Section 4.1) -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -import cx_Oracle -import db_config - -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn) -cur = con.cursor() - -sql = "select * from dept where deptno = :id order by deptno" - -cur.execute(sql, id = 20) -res = cur.fetchall() -print(res) - -cur.execute(sql, id = 10) -res = cur.fetchall() -print(res) diff --git a/samples/tutorial/bind_sdo.py b/samples/tutorial/bind_sdo.py deleted file mode 100644 index 0d52d8c..0000000 --- a/samples/tutorial/bind_sdo.py +++ /dev/null @@ -1,50 +0,0 @@ -#------------------------------------------------------------------------------ -# bind_sdo.py (Section 4.4) -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -import cx_Oracle -import db_config - -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn) -cur = con.cursor() - -# Create table -cur.execute("""begin - execute immediate 'drop table testgeometry'; - exception when others then - if sqlcode <> -942 then - raise; - end if; - end;""") -cur.execute("""create table testgeometry ( - id number(9) not null, - geometry MDSYS.SDO_GEOMETRY not null)""") - -# Create and populate Oracle objects -typeObj = con.gettype("MDSYS.SDO_GEOMETRY") -elementInfoTypeObj = con.gettype("MDSYS.SDO_ELEM_INFO_ARRAY") -ordinateTypeObj = con.gettype("MDSYS.SDO_ORDINATE_ARRAY") -obj = typeObj.newobject() -obj.SDO_GTYPE = 2003 -obj.SDO_ELEM_INFO = elementInfoTypeObj.newobject() -obj.SDO_ELEM_INFO.extend([1, 1003, 3]) -obj.SDO_ORDINATES = ordinateTypeObj.newobject() -obj.SDO_ORDINATES.extend([1, 1, 5, 7]) -print("Created object", obj) - -# Add a new row -print("Adding row to table...") -cur.execute("insert into testgeometry values (1, :obj)", obj = obj) -print("Row added!") - -# (Change below here) - -# Query the row -print("Querying row just inserted...") -cur.execute("select id, geometry from testgeometry"); -for row in cur: - print(row) diff --git a/samples/tutorial/clob.py b/samples/tutorial/clob.py deleted file mode 100644 index f8dc668..0000000 --- a/samples/tutorial/clob.py +++ /dev/null @@ -1,30 +0,0 @@ -#------------------------------------------------------------------------------ -# clob.py (Section 7.1) -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -import cx_Oracle -import db_config - -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn) -cur = con.cursor() - -print("Inserting data...") -cur.execute("truncate table testclobs") -longString = "" -for i in range(5): - char = chr(ord('A') + i) - longString += char * 250 - cur.execute("insert into testclobs values (:1, :2)", - (i + 1, "String data " + longString + ' End of string')) -con.commit() - -print("Querying data...") -cur.execute("select * from testclobs where id = :id", {'id': 1}) -(id, clob) = cur.fetchone() -print("CLOB length:", clob.size()) -clobdata = clob.read() -print("CLOB data:", clobdata) diff --git a/samples/tutorial/clob_string.py b/samples/tutorial/clob_string.py deleted file mode 100644 index c95f39c..0000000 --- a/samples/tutorial/clob_string.py +++ /dev/null @@ -1,35 +0,0 @@ -#------------------------------------------------------------------------------ -# clob_string.py (Section 7.2) -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -import cx_Oracle -import db_config - -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn) -cur = con.cursor() - -print("Inserting data...") -cur.execute("truncate table testclobs") -longString = "" -for i in range(5): - char = chr(ord('A') + i) - longString += char * 250 - cur.execute("insert into testclobs values (:1, :2)", - (i + 1, "String data " + longString + ' End of string')) -con.commit() - -def OutputTypeHandler(cursor, name, defaultType, size, precision, scale): - if defaultType == cx_Oracle.CLOB: - return cursor.var(cx_Oracle.LONG_STRING, arraysize = cursor.arraysize) - -con.outputtypehandler = OutputTypeHandler - -print("Querying data...") -cur.execute("select * from testclobs where id = :id", {'id': 1}) -(id, clobdata) = cur.fetchone() -print("CLOB length:", len(clobdata)) -print("CLOB data:", clobdata) diff --git a/samples/tutorial/connect.py b/samples/tutorial/connect.py deleted file mode 100644 index 948cd79..0000000 --- a/samples/tutorial/connect.py +++ /dev/null @@ -1,13 +0,0 @@ -#------------------------------------------------------------------------------ -# connect.py (Section 1.1 and 1.2) -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -import cx_Oracle -import db_config - -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn) -print("Database version:", con.version) diff --git a/samples/tutorial/connect_drcp.py b/samples/tutorial/connect_drcp.py deleted file mode 100644 index 61134db..0000000 --- a/samples/tutorial/connect_drcp.py +++ /dev/null @@ -1,14 +0,0 @@ -#------------------------------------------------------------------------------ -# connect_drcp.py (Section 2.3 and 2.5) -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -import cx_Oracle -import db_config - -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn + ":pooled", - cclass="PYTHONHOL", purity=cx_Oracle.ATTR_PURITY_SELF) -print("Database version:", con.version) diff --git a/samples/tutorial/connect_pool.py b/samples/tutorial/connect_pool.py deleted file mode 100644 index 0e46374..0000000 --- a/samples/tutorial/connect_pool.py +++ /dev/null @@ -1,34 +0,0 @@ -#------------------------------------------------------------------------------ -# connect_pool.py (Section 2.1) -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -import cx_Oracle -import threading -import db_config - -pool = cx_Oracle.SessionPool(db_config.user, db_config.pw, db_config.dsn, - min = 2, max = 5, increment = 1, threaded = True, - getmode = cx_Oracle.SPOOL_ATTRVAL_WAIT) - -def Query(): - con = pool.acquire() - cur = con.cursor() - for i in range(4): - cur.execute("select myseq.nextval from dual") - seqval, = cur.fetchone() - print("Thread", threading.current_thread().name, "fetched sequence =", seqval) - -thread1 = threading.Thread(name='#1', target=Query) -thread1.start() - -thread2 = threading.Thread(name='#2', target=Query) -thread2.start() - -thread1.join() -thread2.join() - -print("All done!") diff --git a/samples/tutorial/connect_pool2.py b/samples/tutorial/connect_pool2.py deleted file mode 100644 index e319943..0000000 --- a/samples/tutorial/connect_pool2.py +++ /dev/null @@ -1,36 +0,0 @@ -#------------------------------------------------------------------------------ -# connect_pool2.py (Section 2.2 and 2.4) -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -import cx_Oracle -import threading -import db_config - -pool = cx_Oracle.SessionPool(db_config.user, db_config.pw, db_config.dsn, - min = 2, max = 5, increment = 1, threaded = True, - getmode = cx_Oracle.SPOOL_ATTRVAL_WAIT) - -def Query(): - con = pool.acquire() - cur = con.cursor() - for i in range(4): - cur.execute("select myseq.nextval from dual") - seqval, = cur.fetchone() - print("Thread", threading.current_thread().name, "fetched sequence =", seqval) - -numberOfThreads = 2 -threadArray = [] - -for i in range(numberOfThreads): - thread = threading.Thread(name = '#' + str(i), target = Query) - threadArray.append(thread) - thread.start() - -for t in threadArray: - t.join() - -print("All done!") diff --git a/samples/tutorial/db_config.py b/samples/tutorial/db_config.py deleted file mode 100644 index a97a208..0000000 --- a/samples/tutorial/db_config.py +++ /dev/null @@ -1,48 +0,0 @@ -#------------------------------------------------------------------------------ -# Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -import cx_Oracle -import sys -import os -import getpass - -###################################################################### - -# -# Oracle Client library configuration -# - -# On Linux this must be None. -# Instead, the Oracle environment must be set before Python starts. -instant_client_dir = None - -# On Windows, if your database is on the same machine, comment these lines out -# and let instant_client_dir be None. Otherwise, set this to your Instant -# Client directory. Note the use of the raw string r"..." so backslashes can -# be used as directory separators. -if sys.platform.startswith("win"): - instant_client_dir = r"c:\oracle\instantclient_19_10" - -# On macOS (Intel x86) set the directory to your Instant Client directory -if sys.platform.startswith("darwin"): - instant_client_dir = os.environ.get("HOME")+"/Downloads/instantclient_19_8" - -# This can be called at most once per process. -if instant_client_dir is not None: - cx_Oracle.init_oracle_client(lib_dir=instant_client_dir) - -###################################################################### - -# -# Tutorial credentials and connection string. -# Environment variable values are used, if they are defined. -# - -user = os.environ.get("PYTHON_USER", "pythonhol") - -dsn = os.environ.get("PYTHON_CONNECT_STRING", "localhost/orclpdb1") - -pw = os.environ.get("PYTHON_PASSWORD") -if pw is None: - pw = getpass.getpass("Enter password for %s: " % user) diff --git a/samples/tutorial/db_config.sql b/samples/tutorial/db_config.sql deleted file mode 100644 index e1a2096..0000000 --- a/samples/tutorial/db_config.sql +++ /dev/null @@ -1,8 +0,0 @@ --- Default database username -def user = "pythonhol" - --- Default database connection string -def connect_string = "localhost/orclpdb1" - --- Prompt for the password -accept pw char prompt 'Enter database password for &user: ' hide diff --git a/samples/tutorial/drcp_query.sql b/samples/tutorial/drcp_query.sql deleted file mode 100644 index 528f56e..0000000 --- a/samples/tutorial/drcp_query.sql +++ /dev/null @@ -1,22 +0,0 @@ -------------------------------------------------------------------------------- --- drcp_query.sql (Section 2.4 and 2.5) -------------------------------------------------------------------------------- - -/*----------------------------------------------------------------------------- - * Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. - *---------------------------------------------------------------------------*/ - -set echo off verify off feedback off linesize 80 pagesize 1000 - -accept pw char prompt 'Enter database password for SYSTEM: ' hide -accept connect_string char prompt 'Enter database connection string: ' - --- Connect to the CDB to see pool statistics -connect system/&pw@&connect_string - -col cclass_name format a40 - --- Some DRCP pool statistics -select cclass_name, num_requests, num_hits, num_misses from v$cpool_cc_stats; - -exit diff --git a/samples/tutorial/plsql_func.py b/samples/tutorial/plsql_func.py deleted file mode 100644 index 37dd1f8..0000000 --- a/samples/tutorial/plsql_func.py +++ /dev/null @@ -1,16 +0,0 @@ -#------------------------------------------------------------------------------ -# plsql_func.py (Section 5.1) -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -import cx_Oracle -import db_config - -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn) -cur = con.cursor() - -res = cur.callfunc('myfunc', int, ('abc', 2)) -print(res) diff --git a/samples/tutorial/plsql_func.sql b/samples/tutorial/plsql_func.sql deleted file mode 100644 index a5bcf44..0000000 --- a/samples/tutorial/plsql_func.sql +++ /dev/null @@ -1,35 +0,0 @@ -------------------------------------------------------------------------------- --- plsql_func.sql (Section 5.1) -------------------------------------------------------------------------------- - -/*----------------------------------------------------------------------------- - * Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. - *---------------------------------------------------------------------------*/ - -set echo off -@@db_config.sql -set echo on - -connect &user/&pw@&connect_string - -begin - execute immediate 'drop table ptab'; -exception -when others then - if sqlcode not in (-00942) then - raise; - end if; -end; -/ - -create table ptab (mydata varchar(20), myid number); - -create or replace function myfunc(d_p in varchar2, i_p in number) return number as - begin - insert into ptab (mydata, myid) values (d_p, i_p); - return (i_p * 2); - end; -/ -show errors - -exit diff --git a/samples/tutorial/plsql_proc.py b/samples/tutorial/plsql_proc.py deleted file mode 100644 index ef115b1..0000000 --- a/samples/tutorial/plsql_proc.py +++ /dev/null @@ -1,17 +0,0 @@ -#------------------------------------------------------------------------------ -# plsql_proc.py (Section 5.2) -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -import cx_Oracle -import db_config - -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn) -cur = con.cursor() - -myvar = cur.var(int) -cur.callproc('myproc', (123, myvar)) -print(myvar.getvalue()) diff --git a/samples/tutorial/plsql_proc.sql b/samples/tutorial/plsql_proc.sql deleted file mode 100644 index 2e0cb88..0000000 --- a/samples/tutorial/plsql_proc.sql +++ /dev/null @@ -1,22 +0,0 @@ -------------------------------------------------------------------------------- --- plsql_proc.sql (Section 5.2) -------------------------------------------------------------------------------- - -/*----------------------------------------------------------------------------- - * Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. - *---------------------------------------------------------------------------*/ - -set echo off -@@db_config.sql -set echo on - -connect &user/&pw@&connect_string - -create or replace procedure myproc(v1_p in number, v2_p out number) as -begin - v2_p := v1_p * 2; -end; -/ -show errors - -exit diff --git a/samples/tutorial/query.py b/samples/tutorial/query.py deleted file mode 100644 index 404857c..0000000 --- a/samples/tutorial/query.py +++ /dev/null @@ -1,12 +0,0 @@ -#------------------------------------------------------------------------------ -# query.py (Section 1.3 and 1.4) -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -import cx_Oracle -import db_config - -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn) diff --git a/samples/tutorial/query2.py b/samples/tutorial/query2.py deleted file mode 100644 index ab5b1e3..0000000 --- a/samples/tutorial/query2.py +++ /dev/null @@ -1,19 +0,0 @@ -#------------------------------------------------------------------------------ -# query2.py (Section 3.1) -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -import cx_Oracle -import db_config - -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn) - -cur = con.cursor() -cur.execute('select * from dept order by deptno') -for deptno, dname, loc in cur: - print("Department number: ", deptno) - print("Department name: ", dname) - print("Department location:", loc) diff --git a/samples/tutorial/query_arraysize.py b/samples/tutorial/query_arraysize.py deleted file mode 100644 index 39b177f..0000000 --- a/samples/tutorial/query_arraysize.py +++ /dev/null @@ -1,25 +0,0 @@ -#------------------------------------------------------------------------------ -# query_arraysize.py (Section 3.5) -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -import cx_Oracle -import time -import db_config - -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn) - -start = time.time() - -cur = con.cursor() -cur.prefetchrows = 100 -cur.arraysize = 100 -cur.execute("select * from bigtab") -res = cur.fetchall() -# print(res) # uncomment to display the query results - -elapsed = (time.time() - start) -print(elapsed, "seconds") diff --git a/samples/tutorial/query_arraysize.sql b/samples/tutorial/query_arraysize.sql deleted file mode 100644 index 426c795..0000000 --- a/samples/tutorial/query_arraysize.sql +++ /dev/null @@ -1,37 +0,0 @@ -------------------------------------------------------------------------------- --- query_arraysize.sql (Section 3.5) -------------------------------------------------------------------------------- - -/*----------------------------------------------------------------------------- - * Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. - *---------------------------------------------------------------------------*/ - -set echo off -@@db_config.sql -set echo on - -connect &user/&pw@&connect_string - -begin - execute immediate 'drop table bigtab'; -exception -when others then - if sqlcode not in (-00942) then - raise; - end if; -end; -/ - -create table bigtab (mycol varchar2(20)); -begin - for i in 1..20000 - loop - insert into bigtab (mycol) values (dbms_random.string('A',20)); - end loop; -end; -/ -show errors - -commit; - -exit diff --git a/samples/tutorial/query_many.py b/samples/tutorial/query_many.py deleted file mode 100644 index b0ef0a6..0000000 --- a/samples/tutorial/query_many.py +++ /dev/null @@ -1,17 +0,0 @@ -#------------------------------------------------------------------------------ -# query_many.py (Section 3.3) -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -import cx_Oracle -import db_config - -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn) -cur = con.cursor() - -cur.execute("select * from dept order by deptno") -res = cur.fetchmany(numRows = 3) -print(res) diff --git a/samples/tutorial/query_one.py b/samples/tutorial/query_one.py deleted file mode 100644 index 3d7eaa9..0000000 --- a/samples/tutorial/query_one.py +++ /dev/null @@ -1,20 +0,0 @@ -#------------------------------------------------------------------------------ -# query_one.py (Section 3.2) -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -import cx_Oracle -import db_config - -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn) -cur = con.cursor() - -cur.execute("select * from dept order by deptno") -row = cur.fetchone() -print(row) - -row = cur.fetchone() -print(row) diff --git a/samples/tutorial/query_scroll.py b/samples/tutorial/query_scroll.py deleted file mode 100644 index 0b13142..0000000 --- a/samples/tutorial/query_scroll.py +++ /dev/null @@ -1,21 +0,0 @@ -#------------------------------------------------------------------------------ -# query_scroll.py (Section 3.4) -#------------------------------------------------------------------------------ - -#------------------------------------------------------------------------------ -# Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. -#------------------------------------------------------------------------------ - -import cx_Oracle -import db_config - -con = cx_Oracle.connect(db_config.user, db_config.pw, db_config.dsn) -cur = con.cursor(scrollable = True) - -cur.execute("select * from dept order by deptno") - -cur.scroll(2, mode = "absolute") # go to second row -print(cur.fetchone()) - -cur.scroll(-1) # go back one row -print(cur.fetchone()) diff --git a/samples/tutorial/resources/base.css b/samples/tutorial/resources/base.css deleted file mode 100644 index 36128e2..0000000 --- a/samples/tutorial/resources/base.css +++ /dev/null @@ -1,69 +0,0 @@ -body { - font-family: -apple-system, BlinkMacSystemFont, - "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", - "Segoe UI Emoji", "Segoe UI Symbol"; - font-size: 16px; - line-height: 1.5; - color: #333; - background-color: #fff; -} - -#container { - border: 2px solid #078311; - min-height: 103%; -} - -h1 { - background-color: #078311; - color: white; - font-size: 2.45em; - margin: 0; - padding: .3em; -} - -h2 { - color: #078311; - margin-bottom: .5em; -} - -hr { - color: #078311; -} - -#menu { - background-color: #00C029; - padding: .5em; - margin-bottom: .5em; -} - -#menu a { - text-decoration: none; - color: blue -} - -#menu a:hover { - text-decoration: underline -} - -#menu a:visited { - color: blue -} - -#content { - margin: 1em; -} - -.logo { - float: right; -} - -pre { - background: #E0E0E0; -} - -.announcement { - margin-top: 30px; - padding: 30px; - padding-bottom: 15px; - background-color: #F0CC71; -} diff --git a/samples/tutorial/resources/community-py-200.png b/samples/tutorial/resources/community-py-200.png deleted file mode 100644 index b6192d0a36798e311659026fa1df0ea9591026a0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9040 zcmcI~g;N~Q^Y+2v?jGEo1jykUTmr#0xE>r%a19#tfZ##y1HmCdg9ZyBI2;h{a0?vH z;Xl6r!~4!|?^Mlh^>+94JpI(vB)rv8CB&n{0{{So>T1gR&;76e2@cltv(}`p=()qP z)l^jmJpJ$b(p8c9Jc0{RGxY%g@X7xtXn?!|n&&}mUv=#_*z4G|X!LYTZjXil05d>c zS-~Li?@?iJn#Ek`uH5eOQmgfOnhF~(3u!_e8u?^eQ*5K1XBt~#ZwCVyN5RXz%Ab#( ze4XT9)xb^kZ=)l}!oLQ;*KHyl#1F;VgjacmhlmF|mH8Q4L`;7E%C=9Nyc5ft`g3u& z8a)2!^&>dQ(c}8!&&7rBb%Cd0j>?SBTF=D8L)h-A)O-8?mu*%{ro*nH9an`g(>Z|V zxb1^oGqNKBCa-(ChB1k$9Krs+toqmtjkXu463HM;kPKi8SZ?wHU3`{X9(mE~E^6ce zFf~C6(di6`HI@}W{gtB82$WkeiF zc`UZg1MC2%Br+|V9I`&=gjw=6z(*2PYVYM6z(8`H`s=M(jo_)=i_iKRM_B%VwZ)*j z9c$?0UWupN7<}n9z1yn@QO1wM{+&<^K|me4?ANLM$j6VL{eb0A)*S6JXx@Dm{yhLi zbkYGmyC`f8_5LMq2XMh*NIeSNPb`gFz;Oo{9jKW2E3Kc5IJ~lfXc^%JlA}Z#><*9e zt;POx0_396NZn|O)Cal`YcgRxN}VWtA#Onb;lTom>AkjS%KhW+AH)d4xUa8m^?VfX zRnnCng9{Ah)5upTHqM}9MPNs!YBe9i&lrjhGe#Zh8>`uRcO$}=Ivl>ay%&jqX8iNk zoVAnk52IKfOs$8A($eaT09nO2-%Zp(#RN|0*1#w#|YuJ0Zphp zW@>?PBvB!Psw~CD_2%!mIWGe|SDJ1Xp8rqyUfxtI0{XLR*#BQA0A(dI@#TfIs$fn( zA4Q&5{>MlG^H%pT_~%_#w5_QZQm2tmA?GTLD~;z5=#MWc=mO>=oGa-`J3*JTXW8H+~fX{N?mbJ5~vsi5|7n!d3KGgJzc=W0HEb z@Im4v6cIWH?r4@)70*<-346DWGomT$H=jMG>k+)+viU}mH0?Q0lp~ZTWA3Pr5E4** z`&YCV7cU~taaGlyBevY^aPDGP_kzZD#p_%})S_x#=pqnDWWVg|qh%q}Sivc(GNO^rWD-e}nrBX;`+trAC z-6en@ha;_;)NeC8!pMFA%I4q*6RGp$m`I;+Jhf=uUQg3!juD3cO<*iXbVyQt`qKP7 zkKXMq5y0LS_l>E_Mz{_Jd*RqjVSKBWducOg@#scJ?DJdZW92>Ytc<9ufV?y;enC7T z(APsi`BM~I{^0XuNox2ARtUn_89%YYM4y#EXUlq#J^E4;6$#NB!OWp+8E0GR`d#1g z$^2xyB{G<{>sosRK#%KZ#_`sl%rUIKbnVxzzy{Ka)x!8G%2c}r6VRgeE2`Rk9W1Yl z6BE2OtZ-YKZvKEK%=JFKI<9pNsUcEgFVAkG3)EN3c1;r(``y*Fl;nI?)8j z+Scl+O2O^)u^?Ha^UTDOb0iwi;M|m2Ox9?&_whZ=VgpYNVFUieIh~{cv+jE&Jr&{x-pvl z79unB()8tGh;WH3-Q9@OD~e^snPL7pJ2<*LPH8%hH+xxTGARf_|uAL{@^T)YFO z-82CMR+>s@xaLP_yd=A=UWoNNAJc>}1s-F&Vq|(csl`0u`>DQJ#i_R0EqWK-M;CN=lL z+HWKJE<>U9Z_P?VKGjF<*u$!bdvf%dNfg3kC>Hct9T0^4ql&CUt+TI~20RhM5r3o@ z=2nlh!Z~5=oE)H1v|%A}3g@>zZCJoKwDNt(EIF0H>?z^1$Jw!EHd4Y68u=Yk09ZU54zrotVY*Y1or`YM=K7?5DL=jdM@MA8)kl3(#m5} zR#;8H9sO{;`0j(jO}j*QE!`zNzy62=^7*~q4A2sESPQ??g)JPG9E(xkp|1EGX8lPqQ4=|L~}>Ttoq1hC{Sk zG_eC2QRa=o*Fscy&=16oDz9j>{R6yEv!*&lQWJUGp$pjsfyIA?Yo-+u)_{s*C^VL zLoZQ59;5YRK}upjj>;?Ab`GC3+yg611PmizxGYD-@#<7f59W-rek}85!b1r_Z?mm> zR>?fO<|G))%flvS* zdh@UDoM9(2OcR$;M_Gy_P&7L02(ORDrPz5hPIY&mQ+*;*(TWdfh+metDB{5Rt%nj} zGL;(`m5o{{Wfj;Tkh7*k)2949yVOvLUK>ihnhy@t)8|86h0ube&o__FMBaoG?=~Iu znpJRYq*X6Nl_PsFGR7jx+q=F=a{qxStdTJ+?QFH5N2s`H^JcPTjXXnw#bj&Dv2qOz zA&*1mq&pMvp2BvWk?`Fh(Bo3H4#+sRFNml7gA|;Q@)T!?XyP||_*bFu&SSg#Acl<_ zPC_wlq!D)-2c`1%;@WyOJ;*J+18Ls5TbP_zm$8eZ!mi1}7346}9W- zy(e7}r>ZtkI#5P*TB*c(K$4`Act?_zKHk}f_NBZ$e64|`|izv}jQ8U1V539xj<*RUX z9<`w|iD^>jhKFc7xG=%cV85R^P9JtGueim22ZJ#W8nxp0k2bAL4BCdHge7pZ%=kC$TR$nc zEOVxOvkk;myAkODE8caiG2G!$yo~!st-3w+DErs8rVIXWJGFm*(MaHMXtY)L(}Kbk zC@E@BvwmJvN=QYWi52Y{`}WR~&n4J~iG+0vI2d51T%ng^B-D$wh*?=SB7!KSQ{B7e zIdixQ1rWTg_d8UuJOgy!Db+xYbt;OzMg9ee(~@=_2LW;!?dTltT`Jg9Yq=gR=r4I% zgY;m+0*ExfF0khQhr8@VZl6L^J9@4K+WdJGm=~`t>tJJ?fk*_F^|kaHF9i;ZyjE*V zTx1y-;fya|)m6e`5?@hYK|e)q(r`w36Q#N8!_y{3k zOD#f)F15p))ULlz&ytI-KNcrjVHno@ze0L0TW_ z(hsuv^pe}j6XE>Q-cdw)r(IOHx>G=kFol!Q9gM|nYgOES3;|KipQL9;mGMMy!&G** zWd*P%rqm@=%dTd&-el~&NYkFTJfsI{DXykK{zx&eEuWGB7DHH8D&5SX5N9Cv`P5>^ zWJJjA4n2TIS}q%^NC$FkCl;HzT`Mf|5uM8a6Z{G(HLP>-xa9U@WanI&EnR}&dIlY` zM>q5;)ziZ8-<~@khz{3up601+iY0mTuzY8lxXl^*$l z?e^YrqE8tA*okw{(D@j(k0K)h^7`^W@FA6!Y#3ZT6)v5Mm4Y+?U?i5$DDq z1d^76^v$3(El8`9+Y|-Jhgg$-TDZ9MErV`sA%uq~3i7XN+di+qHQNjd`(Tx1co^HX zFl3aKGN&N3kHccp{XM^uT)}N@@!XC(f@fJ(B?kIyFuwAKHV;j zER-C5WaFaIc5-^i_&jgUNe78Yj5DjM+Up1t7J_Zyxi5)Bw6Ikxd?`NixSIcOS*H}g zuoW#N^d@EbwRO%Q9hMk4TN3Ujh!kc&9MLBZwmZAm64E0=XPus{C^fmr9 zI8bP)lp2^sy>2(Ozen=(p~Byam4=n&dEz()G1@>xai=lSl&Q(D*UT0t)FemSr$kir zb9-Ux3gdnfL9S`>;LK!I^?_1$yS|=m==Q;`c5IpNmmtPun&Q$*N@v&*U(4Z1RiY73 zIbJ@{;^nb$y}ql}(La)zClgHJ*;&$911NBvw^?dd#fK)U z8~^ZtMLonReDQ-24*o@(`Kc#R?rNKP7B$X&(fl7DDI0BS#{QVk*|P9gcOo7ReT@*< zbI6|WVf>*%hK@|!4swxlv7&E#j>`>_sU@M5{D)sCD-VivaT<6>KYi@`=8c&U{^+!I zFqh6_ZAKNcNiUl!y)m;U6nLVL&sUc=uu1-D^qM6$&(z=}1+(gs(QQ_8+^%i(Msq2jtbdiK6sDU!Q4UuQ=tIJI=|BLr@PM^fG zrXS`Ij}T0~rK6)J`6ds_`oQPeV;#s`*X?r2-_~S1k~Q z0qCAD_mTI-4_c;OUE*C`>s-G3u!fL!#6Qy)L9gI-loo*{%;`8zd+odTwAY8C5;d-Z z%zj1;{LQ``rFwz_M#c>4PHIj*TX>9hNX#td7!{m^NxJ!K+OcvPI_;7v6a9?^@$`7@ zftIMBUfKNAvDuW{AEJJ9*lL*RRl&@w_9}hfZk4>nNrc~Y=beOh&0F#k+EY#_O`NT2 zPn~V|(f^=hc$eqE9&(;fDtpP36OGNK$coWEQ_)Dw1fObldo#CLYsLZcQPIUMgGOC< z>CT=Ju}K9~b-RwvHOr4TA{do*-N^fvK*!4QsyPVi(c~RG!LRObWf~D3Z;)TOXr$%) z@Og!Gn(*7IrU}tPSa>@qa8A z7dR~*&286Ksp{9fbx@tBEiT>;e{dV6kR=|UvC-P~AT@p!zvIEKHwf8TR=vYz^PZoX zK^t3n;=(g2=^vE$htR%G6r4i3__+k88zhDZ(${>`GvycDLc>^n@6XdkGHVzb+NqWH z**x$yd35}+Q$X_CgXPGxS|Wze5aIQXz1M5A%VEKnV4)vcXFu^>HLA^6^<*e8ygj>M zb~f$W3zyG)B8O0OgpcO(9zk{+&+-$lxc{aa^(?TRck!v+MYoN-E-PqmJF zn-gjj=DGGF1Nk&Xm_ibbh`~y~68W&Fu)?Qpp{)2pW!ru%n|dpON-E^_6`r_z8UqNS z+dX{M7yXx>Vb(u9ysh!+$`jkF^zG+=mQg3wel=GDjPfM!h z6M4d9K7oht+hN|y%Dp?h`UT_{-aPZH>GcP?yt8TiCfg&TLbdm%WZIr^62;!&#~+&QDwF{~jMtj#i(-6igVW zW`LtP#x9HJq<7bD>mATmuoox~J>G*V0+uqc@hMA@6|6o`cOttkH!FK?E4wiqRrF*O z!}jnKpZBiT^TsVx9ZdO2V7DV#`YfL5X)-RRE93)e&|`tQe_Vlx$n~i(K-Ab;K=C{_ ziw|>UB}&WDR(5Jdz@&x$!sKz11 zRqd?9K3ukr-<+HE+|||L$+Ni;x6(3~b~RSn%#KAU9saFA8@NW!+kC>og}n{FhRgPf z&Bu?(Ed6ZJ6Y8lsc`%Fkg}a1{W^848X?ln3u0HbF^&N|h6982=(nbWn01jKs-xrNZ zZp^GK&s3-f77;DMpS=l2&Uxfm{V>Xo`)V)?@K1R$6Z>#iUEHfG=c>o8wS4@5tasII zr?xiGaqzj=aw`~nIK+?-3-9lBR{fAdgVFtye{o`q+mpAp#?csnAy=+m`6|L^`}v@aCA^Hk7oBB%qh6Rk@q@* zPpX{Jp$GuEB-K*oN8@*Z#qTz~nB4^b#2|mP^hVDHEDmhPy!@@*A!$EHGS{t#Hlz6J zNPPV;m5!C5A8s77YC1>2Mqlc9BBY;g8O6agfy5{R$aJq>v2FJDU;WCwy9RKjSa{cu zADn2e?ppIv1GFy;kC!~-g40b1;I-ryKH7#-`f@i3O?3Mbc?2diLDNZT_vmdzAP+=k zZ5z0qG&0Yh>9no4NqKJ5IJeC}95FTZ*NQz$(fH8)`_cO-gRC9P?GGkBkIf#f4X!MT z{LeOUD|i!_5O;ouBm^J+WRd4`-~W5LnqMbabtS}Y=!24wpWAPxlt>P|18-O8uZK4{ zenQwW;jcsnqZ(2QWU80iJs|IfZ4KpLVk~3a%Rov)5tZN2%=kD(Rd#*m9q&*rX zVGnNLikVsd$;r|pyFNfJ25!DM!9UF7pvOy@YjROcPuDk~gCHZDbEJ<%srCKR!MB~F z+msDhq2SNlE(HPt4Dw>mROrz$k>k@`T2Fu{SU#c>{OWAkhnJ2@!w`4EKsGh#P{2WR zu>~S3Gmu9xQjYQ1`yV0jHTpat6&UBdAU~|qr{`%xIlXJ0XKi_dmPYv5ENBT##={lF zvqmg=CfN4n!0~vJjKm6&lCN}D>Fam4_p&&m^n-mzjSuF^!FB{ z(MLX^hX6rvx+0~RaNkR$Lv3wh{j#(wezP4Mua!Q2qFGn zTQkmZ@O7#{Oq0MUHtRW~hWnyzqe~`x4u0o5W9nuE08$)klpvY9MwujX@#ht9(1eU% zd(59Cp(>77V=`3xA!(*@R)bHwk>8%<-P!bPJN0uqw&An^Q1x`(@7ysp(Ik3d-gWN% z>o~;K%BOa?1_pRSF6ynLEoUN{9WA|{f5#4y)HSATCS7LFl=%^%0M>n-Tcw{8ecMNT zK=kbbUkLR|W8D0E+{%B12DDL>PvLZ^wLxNC*=r; z|6zzx7-_e(B7wS#r3>y?4Cbf|WsRt8MQ5Ivx~U7X@FVYb23D&4Q~!NrsP14Y<8%1f z$`x3ip=q2+uCA=wc)pX<)0e_+f>B#n*Svp9Ji_2yP`3y5GGFdd&!g+~T8BbrBngHr z3OR-;lcp$>Lb*wVtb7GB znL``$zM0p4o#2v#$sJa_*sQtT;(49(ry-VyC5CaSSyQ#u{p*L*TxzSgE0b{U8@Z^y(i*XegSe-7?C}ep?;f8dklxzOWpOUq3ifX;m9r{Wbxxy zv6aT5q9BdJhaWxXMwIw56MF_(QS(;s+!ytIl&fw7K zR>534!uKcrVLPgF`lB0O8~WoYTMQBjf2|Mw{eP`GL_-3D1>+t@t$F{8uGdz%^k-le zs8Ejj7O-}dKvFg-pSnq+$?4=NY&OFIYazoGL0!x|t~(X!r6&ndU7c-jm(U&Oz6yLU zo#ZL~RmxEV{0MHIA~l>sEhE3+MPqA2_4&C%!1E3*?Q~wVj7*0H>F}2wPapxOAk1oI z?u`OZp-fo8&#AKQ*05G6dr)w*z(7I1z@Xneu3nkn@27p;=0A?Ro(`*=N{#>*bmnx< zy8RcK84C>3%3~HTE0TTiT0`&y&t(u8k&94y0az^Wp6`R7keRmz!owl zmZ2N|w@S=+K+0Ebv@5vz^91!zTT`3gy?VQ{ROm0);`r*J_cL;1zhs-%9_os8sh z+P{-N20;#GMzMA!UGc_GCVI#{y~3y4`Ou2!=iO);5Z#WK&f`t-ou;kGlse=yNi{j* zbA4dX`1#31(e`ZDbf+FCT$Mp;=0^2EllS{CU}ht;H+lUU++ zxMfegotol$jZmmf>Oc4)khGl*bC9`KbTetudMW(A(RMJ<@aFfC##45pZCq&mC10;L zPCAwKW78?~3#5eWk!-PQ*Uf)t)eNfY3H}8`HMrL8{=@A)mG3H`MPyUBw%KoHThukw zJ8!vn67D_?Vi;RGcnwk-wSO6KM?}m|-imvzg}RX2<`w|WaRlQBtFp&&%@4ascS*nT zcS*93*9UdF&X-WIXGW!lu>U;UR%kpNaf^zHHL~f6Gmcmq2)!Tuznl_*pi*u zzO&1eS{s^&7Wex>rH6;*7+G=|;_Uow=29cgpFf7>W2)Y)(l;x(8CF|q)*iuPh{G2+ z(@EZ$)i_cabtS^{ad&n^35hlp2pG;gy3AU~GaeqAmdnjoqzv%C${!j}-THeyJ@n&y zB_(5vK>mzgeRZZlPHPIp;2s=XqPiv(8!-wL{A z^~6y@ox=lbJ<0HJX2w87!Q)|%%5p$4+Cb$4%b4cKh?+8gJ0z>1;qg`r!7a8edm@h| zza0J)wIT@orS-{8T$6JC$CeLY_(2;HCV>h=$#>4i8pfj#D*ym}>VIbetf6<38cQ*; z2Jd>pG1q#urBMi;(CZca$6tiZmk_Ug*tfN|Gl9Q&kcmydtyp#efwy!OKQv>4(!Qz( zZX?48mifESvNmt)AMs9mf%sudT6{(?$iBTe|EA`h;(cthw7GK7e4ZriY>g?4n~WBZ z&E%e7?gS9Q{m|c{Yc3x31-=D+TSLWQ*7FhGngm`_F*_2;BXM(NF-i==3KUcXY2e<* zfVp;QGksDJyO!LSakfh>cDFvo{ebCJ7Wm;z(8?P;Y^|z~7S`j9_$c7%q>|I6WdKX} z86{9H|L|+xwL59Q$mu7t4F%3tBbNs{jTPP&Fn*qQE7)xW+XIlB+l_Cp)XxtbHb~VI zxQ;N^JJ22ICoY~dZ*92%zK)o^`%~XZh7dgg_liJfaYtsuGMUOQ1vb{>Quk}kNr`3b z0sHaN_G_{O-JzHWH-oxAZivsHV`46$sr_eo8lmVkXl9H=!egD^(6+@wx3h?W;i>Rv zxX3LrUQW?}6zP7Ne|zBH7fv{r!>5OqDtW=a)rXgQzV|vi{&) z%Wn_+gI*=rqni6#n8n%$F03@7=b}kz3}w830vrqOu;IVEFU|5!X$ZwWIYRd!TWT$y z4p>b<9T*cbzaSiWe1I{9dLeqFdj)!>D#cdBoojD(H>#}(^SqY`h=d)& z>Fn5YtXH)gZjV3p2EBjLH|?2(mtq~@XXo-sKatN9)n&{>;X!SE_gCa$ae}qQ({t3x zE#soSKN19aPk@rUJsa&&Z$S4H>XVG`@Db{~h2gTWD|T{qUp3Dh5yYkc|7ueh58+BS XtoNsh%zDpfUjXWFbd>89-$(r)){=>n diff --git a/samples/tutorial/resources/cx_Oracle_arch.png b/samples/tutorial/resources/cx_Oracle_arch.png deleted file mode 100644 index f10e56da1e799b0db795b0e8827d35c28d20b619..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 130379 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 z

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

  • MdW8fP3UpE#aBs!I4-4PP9ltXf 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

    mi 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 diff --git a/samples/tutorial/resources/favicon.ico b/samples/tutorial/resources/favicon.ico deleted file mode 100644 index da454326632dab465b304ea3c38064aa0eaeb244..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18094 zcmeHNdu&uy8oy;JyGxcxSPgNLmA-L-LbFzszpNW2#$Xm7Ev{BTh22F{Us3)*BT)fO zcTFlFK9HIyF$P66@r72Dh>s|0t<{PyNO_b#p)E|?nd!`Lzu!Ig%sq2wrtM6-8-w>G zcg~%2&Ue1Y`ObIF{l3q#vaDZOlP6nJ=UC@vTh?65vT|~q|7Tj(VreTWasC%u*1EGT z>s%QlAOfM*w?ogka^L6q=$2U14eKg~h&MXf2-Aj}!mnC2fK{vd zar4bUAP~X!?I*EoR{*7@GOy4)fBpb~C>H)TIsB7P0#7{!EL|$Yq}+24@a?z2>#qy% zLi589_uK6?HH0^1qty{%1+76V0| z!;i+_-5rs&l}nsa_U;|Pqi%KsS6_{RCg5k!+iwH6 z-U{s7=Z-5c7oBnD9Vhxjp3PEGK^+{K4%~e=@X9N|i!TCGg@052y#EzS8*$xuql=-Y zhW9Qtf3D~)c{a*z1-VVcivVQ8nmVa|I@H+?pym>B$9Xm8{5mZ*HweRZ#KKVqA zsjnZ9d#5X-WO!1wnfRNUqL|>|FDZ%IHh~zpCN_LQKc0K;l#TSj0pN~16n@@&i;70B z112pd{_nmEV}i5aOG-k5KWZb;_$P|}+rK|(x4rWYP*S4o78@2XCjJ!PC_}7^iNCoy zj7fQ2_I|tYLK+2ypE}F+pEvJ>eB-FLfB#E;E3~%S-J;t3hBOQ9z+@ToM|naHNrw&r zw8i9u-<`Vf-Q;Z~@0^?Rt*r%aza6;hD&W&kfggSVxbDTpYEIVAKC8y@drr?iYnEyw zJikG8`)KMcp-J;Mnt6QKu~`paj=mgy9mr4z5*z}4gD(MJ0>2aq&`&5K7Hg?IF+ZWi zS`x|#@2kEY&d?6~@*R`Axvzh|{+%{DKtF75i~HFA)-=}Fj`eDI@vG7oThRCa+h=(a zm!=mDC}Ss8Hk)xbj!L45L>dR5ofO5O{7xk>m=t1T`1WvxtmV(v+{r;L?Fv39NUt!gfg z9SdVh9_@k1WE4CsSa4GO^j+fH?6T{*7r6G>EB`m&?8A7WdHLVF#P=D%o;}P%31Ip1 zPW+m@3{$Ier(<9Q3Qt=@-~Sv zQ|>3PJY`?qcb^l4)Wl2u*kck?$iSy=&Tq!MIQ8n)sp$`gIU;pgvqtt#lCvpG_RQ|x zjuYJg|ME+T3ypFQq(eVZ(}%xtqw=ZixUMGY$dM2xC(s|oIilx1Js!uu05Kn|pdfMI zoAMp*ov|actX-R0{v2oe@Nc-mgyee^Wn~^)DtT>-7A5MwhODgQohY-|AbDG~Na+mq zhR;P8rKX?qW=vDdgt5)ePECobtExP-h)vqEMUu;m5{K9+QFpZ587yBRGCA{=A7N=>~_3qs+ zEqr_C8FS1a&Og5`7UyI?)?V@x%9{$^~h4D>0He(JWVAA_i?b9IB{#L+Ldap%rJ z9I;ni0WhX##v!+E<=rc@`1VlzdFkJ?N6sem&L!VcAAAs>>#${umVXpe#cp z;*n99bIbf&#y@p#1G{-2J@3zBzX!bZGe`F^?TbnCi(!!hvTaLox#*J<~ zo_#O9TTQf?5ZX|yLQF5>3{Y0uyf^zm;1A>PWPyq{me7hbn)zF4ux5GHt7DbsLRWNb?e;c zl1qRof|vJC+LD$Q*m?LhH7@PM%N!@>5}L3T6^XPhSrR`+=iH11?ZeGMJ4jx1KGtyK z{r<%D^JU`8#Mc2|2Yemyb->pFUk7|0_!;X!LPC(=;7h=lfG+`G0=@)%3HTB?y(REp DZ5H5R diff --git a/samples/tutorial/resources/python_nopool.png b/samples/tutorial/resources/python_nopool.png deleted file mode 100644 index a39477764c091f7a317d01b2c772f68d5707b6d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 243864 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≷TRvCz37A$FnJn^ihq(EvIy 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