From ed415208f67dcc1887953e9d9dd3ff2a7ea7ea5c Mon Sep 17 00:00:00 2001 From: Nicola Coretti Date: Thu, 5 Dec 2024 15:44:30 +0100 Subject: [PATCH] Add support for GH-Pages (#159) --- .github/workflows/ci-master.yml | 4 + .github/workflows/gh-pages.yml | 32 ++ doc/changes/unreleased.md | 9 +- docs/BEST_PRACTICES.md | 118 ----- docs/DBAPI_COMPAT.md | 88 ---- docs/DEPENDENCIES.md | 21 - docs/DESIGN.md | 13 - docs/DEVELOPER_GUIDE.md | 27 -- docs/ENCRYPTION.md | 65 --- docs/EXAMPLES.md | 65 --- docs/HTTP_TRANSPORT.md | 163 ------- docs/HTTP_TRANSPORT_PARALLEL.md | 37 -- docs/KNOWN_ISSUES.md | 25 - docs/LOCAL_CONFIG.md | 35 -- docs/PARALLELISM.md | 28 -- docs/PERFORMANCE.md | 67 --- docs/PROTOCOL_VERSION.md | 27 -- docs/REFERENCE.md | 785 -------------------------------- docs/SCRIPT_OUTPUT.md | 61 --- docs/SNAPSHOT_TRANSACTIONS.md | 27 -- docs/SQL_FORMATTING.md | 93 ---- docs/img/parallel_export.png | Bin 100929 -> 0 bytes noxfile.py | 1 + 23 files changed, 45 insertions(+), 1746 deletions(-) create mode 100644 .github/workflows/gh-pages.yml delete mode 100644 docs/BEST_PRACTICES.md delete mode 100644 docs/DBAPI_COMPAT.md delete mode 100644 docs/DEPENDENCIES.md delete mode 100644 docs/DESIGN.md delete mode 100644 docs/DEVELOPER_GUIDE.md delete mode 100644 docs/ENCRYPTION.md delete mode 100644 docs/EXAMPLES.md delete mode 100644 docs/HTTP_TRANSPORT.md delete mode 100644 docs/HTTP_TRANSPORT_PARALLEL.md delete mode 100644 docs/KNOWN_ISSUES.md delete mode 100644 docs/LOCAL_CONFIG.md delete mode 100644 docs/PARALLELISM.md delete mode 100644 docs/PERFORMANCE.md delete mode 100644 docs/PROTOCOL_VERSION.md delete mode 100644 docs/REFERENCE.md delete mode 100644 docs/SCRIPT_OUTPUT.md delete mode 100644 docs/SNAPSHOT_TRANSACTIONS.md delete mode 100644 docs/SQL_FORMATTING.md delete mode 100644 docs/img/parallel_export.png diff --git a/.github/workflows/ci-master.yml b/.github/workflows/ci-master.yml index 6e15ea3..598c34c 100644 --- a/.github/workflows/ci-master.yml +++ b/.github/workflows/ci-master.yml @@ -18,3 +18,7 @@ jobs: ssl_cert: uses: ./.github/workflows/ssl_cert.yml + + publish-docs: + name: Publish Documentation + uses: ./.github/workflows/gh-pages.yml diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml new file mode 100644 index 0000000..3193f79 --- /dev/null +++ b/.github/workflows/gh-pages.yml @@ -0,0 +1,32 @@ +name: Publish Documentation + +on: + workflow_call: + workflow_dispatch: + +jobs: + + documentation-job: + runs-on: ubuntu-latest + + steps: + - name: SCM Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Python & Poetry Environment + uses: exasol/python-toolbox/.github/actions/python-environment@0.18.0 + + - name: Build Documentation + run: | + poetry run nox -s docs:multiversion + + - name: Deploy + uses: JamesIves/github-pages-deploy-action@v4.6.0 + with: + branch: gh-pages + folder: .html-documentation + git-config-name: Github Action + git-config-email: opensource@exasol.com + diff --git a/doc/changes/unreleased.md b/doc/changes/unreleased.md index 28f09ad..1eea4e3 100644 --- a/doc/changes/unreleased.md +++ b/doc/changes/unreleased.md @@ -1,6 +1,13 @@ # Unreleased +## ✨Features + +* Add support for multiversion documentation + +## 🔩 Internal + +* Add support for publishing documentation to gh pages + ## 📚 Documentation * Add sphinx based documention - diff --git a/docs/BEST_PRACTICES.md b/docs/BEST_PRACTICES.md deleted file mode 100644 index c2f5b08..0000000 --- a/docs/BEST_PRACTICES.md +++ /dev/null @@ -1,118 +0,0 @@ -# PyEXASOL best practices - -This page explains how to use PyEXASOL with maximum efficiency. - -## Enable compression for WiFi connections - -Wireless network bandwidth is usually the main bottleneck for laptops. `Compression` flag enables zlib compression both for common fetching and for [HTTP transport](/docs/HTTP_TRANSPORT.md). It may improve overall performance by factor 4-8x. - -```python -C = pyexasol.connect(... , compression=True) -``` - -## Use HTTP transport for big volumes of data - -It is okay to use common fetching for small data sets up to 1M of records. - -For large data sets you should always consider [HTTP transport](/docs/HTTP_TRANSPORT.md) (`export_*` and `import_*` functions). It scales well and prevents creation and destruction of intermediate Python objects. - -```python -pd = C.export_to_pandas('SELECT * FROM table') -C.export_to_file('my_file.csv', 'SELECT * FROM table') - -C.import_from_pandas(pd, 'table') -C.import_from_file('my_file.csv', 'table') -``` - -## Prefer iterator syntax to fetch result sets - -Iterator syntax is much shorter and easier to use. Also, there is no need to check for `None` or empty list `[]` to detect end of result set. - -```python -stmt = C.execute('SELECT * FROM table') - -for row in stmt: - print(row) -``` - -## Avoid using INSERT prepared statement to import raw values in SQL - -PyEXASOL supports INSERT prepared statements via [`.ext.insert_multi()`](/docs/REFERENCE.md#insert_multi) function. It works for small data sets and may provide some performance benefits. - -However, it is strongly advised to use more efficient `IMPORT` command and HTTP transport instead. It has a small overhead to initiate the communication, but large data sets will be transferred and processed much faster. It is also more CPU and memory efficient. - -You may use [`import_from_iterable()`](/docs/REFERENCE.md#import_from_iterable) to insert data from list of rows. - -```python -data = [ - (1, 'John'), - (2, 'Gill'), - (3, 'Ben') -] - -C.import_from_iterable(data, 'table') -``` - -Please note: if you want to INSERT single row only into Exasol, you're probably doing something wrong. It is advised to use row-based databases (MySQL, PostgreSQL, etc) to track status of ETL jobs, etc. - -## Always specify full connection string for Exasol cluster - -Unlike standard WebSocket Python driver, PyEXASOL supports full connection strings and node redundancy. For example, connection string `myexasol1..5:8563` will be expanded as: - -``` -myexasol1:8563 -myexasol2:8563 -myexasol3:8563 -myexasol4:8563 -myexasol5:8563 -``` - -PyEXASOL tries to connect to random node from this list. If it fails, it tries to connect to another random node. The main benefits of this approach are: - -- Multiple connections are evenly distributed across the whole cluster; -- If one or more nodes are down, but the cluster is still operational due to redundancy, users will be able to connect without any problems or random error messages; - -## Consider faster JSON-parsing libraries - -PyEXASOL defaults to standard [`json`](https://docs.python.org/3/library/json.html) library for best compatibility. It is sufficient for the majority of use-cases. However, if you are unhappy with HTTP transport, and you wish to load large amounts of data using standard fetching, we highly recommend trying faster JSON libraries. - -#### json_lib=[`rapidjson`](https://github.com/python-rapidjson/python-rapidjson) -``` -pip install pyexasol[rapidjson] -``` -Rapidjson provides significant performance boost and is well maintained by creators. PyEXASOL defaults to `number_mode=NM_NATIVE`. Exasol server wraps big decimals with quotes and returns as strings, so it should be a safe option. - -#### json_lib=[`ujson`](https://github.com/esnme/ultrajson) -``` -pip install pyexasol[ujson] -``` -Ujson provides great performance in our internal tests. It was abandoned by maintainers for a while, but now it is supported once again. - -#### json_lib=[`orjson`](https://github.com/ijl/orjson) -``` -pip install pyexasol[orjson] -``` -Orjson is the fastest modern JSON library. - -You may try any other json library. All you need to do is to overload `_init_json()` method in `ExaConnection`. - -## Use [`.meta`](/docs/REFERENCE.md#exametadata) functions to perform lock-free meta data requests - -It is quite common for Exasol system views to become locked by DML statements, which prevents clients from retrieving metadata. - -In order to mitigate this problem, Exasol provided special SQL hint described in [IDEA-476](https://www.exasol.com/support/browse/IDEA-476) which is available in latest versions. It does not require user to enable "snapshot transaction" mode for the whole session. Currently this is the best way to access meta data using WebSocket protocol. - -Also, it is possible to get SQL result set column structure without executing the actual query. This method relies on prepared statements and it is also free from locks. - -Few examples: - -```python -# Get SQL result set column structure without executing the actual query -C.sql_columns('SELECT user_id, user_name FROM users') - -# Get list of tables matching specified LIKE-pattern -C.list_tables('MY_SCHEMA', 'USER_%') - -# Get list of views matching specified LIKE-pattern -C.list_views('MY_SCHEMA', 'USER_VIEW_%') -``` diff --git a/docs/DBAPI_COMPAT.md b/docs/DBAPI_COMPAT.md deleted file mode 100644 index 526a0cf..0000000 --- a/docs/DBAPI_COMPAT.md +++ /dev/null @@ -1,88 +0,0 @@ -## DB-API 2.0 compatibility - -PyEXASOL [public interface](/docs/REFERENCE.md) is similar to [PEP-249 DB-API 2.0](https://www.python.org/dev/peps/pep-0249/) specification, but it does not strictly follow it. This page explains the reasons behind this decision and your alternative(s) if you need or want to use a dbabpi2 compatible driver.. - -### Alternatives - -#### Exasol WebSocket Driver -The `pyexasol` package includes a DBAPI2 compatible driver facade, located in the `exasol.driver` package. However, using pyexasol directly will generally yield better performance when utilizing Exasol in an OLAP manner, which is likely the typical use case. - -That said, in specific scenarios, the DBAPI2 API can be advantageous or even necessary. This is particularly true when integrating with "DB-Agnostic" frameworks. In such cases, you can just import and use the DBAPI2 compliant facade as illustrated in the example below. - -```python -from exasol.driver.websocket.dbapi2 import connect - -connection = connect(dsn='', username='sys', password='exasol', schema='TEST') - -with connection.cursor() as cursor: - cursor.execute("SELECT 1;") -``` - -#### TurboODBC -[TurboODBC](https://github.com/blue-yonder/turbodbc) offers an alternative ODBC-based, DBAPI2-compatible driver, which supports the Exasol database. - -#### Pyodbc -[Pyodbc](https://github.com/mkleehammer/pyodbc) provides an ODBC-based, DBAPI2-compatible driver. For further details, please refer to our [wiki](https://github.com/mkleehammer/pyodbc/wiki). - -### Rationale - -PEP-249 was originally created for general purpose OLTP row store databases running on single server: SQLite, MySQL, PostgreSQL, MSSQL, Oracle, etc. - -It does not work very well for OLAP columnar databases (like Exasol) running on multiple servers because it was never designed for this purpose. Despite both OLTP DBMS and OLAP DBMS use SQL for communication, the foundation and usage patterns are completely different. - -When people use DB-API 2.0 drivers, they tend to skip manuals and automatically apply OLTP usage patterns without even realizing how much they lose in terms of performance and efficiency. - -The good example is [TurbODBC](https://github.com/blue-yonder/turbodbc). Very few know that it is possible to fetch data as [NumPy arrays](https://turbodbc.readthedocs.io/en/latest/pages/advanced_usage.html#numpy-support) and as [Apache Arrow](https://turbodbc.readthedocs.io/en/latest/pages/advanced_usage.html#apache-arrow-support). - -Minor intentional incompatibilities with DB-API 2.0 force users to look through manual and to learn about [better ways](/docs/BEST_PRACTICES.md) of getting the job done. - -### Exasol specific problems with DB-API 2.0 - -- Default `autocommit=off` prevents indexes from being stored permanently on disk for `SELECT` statements; -- Default `autocommit=off` may hold transaction for a long time (e.g. opened connection in IPython notebook); -- Python object creation and destruction overhead is very significant when you process large amounts of data; -- Functions `fetchmany()` and `executemany()` has significant additional overhead related to JSON serialisation; -- Exasol WebSocket protocol provides more information about columns than normally available in `.description` property of `cursor`; - -We also wanted to discourage: -- "Drop-in" replacements of other Exasol drivers without reading manual; -- Usage of OLTP-oriented ORM (e.g. SQLAlchemy, Django); - -Unlike common OLTP databases, each OLAP database is very unique. It is important to understand implementation details and features of specific database and to build application around those features. Generalisation of any kind and "copy-paste" approach may lead to abysmal performance in trivial cases. - -## Ideas for migration - -Find `cursor()` calls: -```python -cur = C.cursor() -cur.execute('SELECT * FROM table') -data = cur.fetchall() - -``` -Replace with: -```python -st = C.execute('SELECT * FROM table`) -data = st.fetchall() -``` ---- - -Find `.description` -```python -columns = list(map(str.lower, next(zip(*cur.description)))) -``` -Replace with: -```python -columns = st.column_names() -``` ---- - -Find all reads into pandas: -```python -cur.execute('SELECT * FROM table') -pandas.DataFrame(cur.fetchall(), columns=columns) -``` -Replace with: -```python -C.export_to_pandas('SELECT * FROM table') -``` -etc. diff --git a/docs/DEPENDENCIES.md b/docs/DEPENDENCIES.md deleted file mode 100644 index 6c70474..0000000 --- a/docs/DEPENDENCIES.md +++ /dev/null @@ -1,21 +0,0 @@ -## Core dependencies - -- Exasol >= 6.2 -- Python >= 3.6 -- websocket-client >= 1.0.1 -- pyopenssl -- rsa - -## Optional dependencies - -- `pandas` is required for [HTTP transport](/docs/HTTP_TRANSPORT.md) functions working with data frames; -- `ujson` is required for `json_lib=usjon` to improve json parsing performance; -- `rapidjson` is required for `json_lib=rapidjson` to improve json parsing performance; -- `orjson` is required for `json_lib=orjson` to improve json parsing performance; -- `pproxy` is used in examples to test HTTP proxy; - -## Installation with optional dependencies - -``` -pip install pyexasol[pandas,ujson,rapidjson,orjson,examples] -``` diff --git a/docs/DESIGN.md b/docs/DESIGN.md deleted file mode 100644 index 74ee496..0000000 --- a/docs/DESIGN.md +++ /dev/null @@ -1,13 +0,0 @@ -# Design - -This document contains background information on various design decisions, which will help current and future maintainers and developers better assess and evaluate potential changes and adjustments to these decisions. - -## Automatic Resolution and Randomization of Connection Addresses - -By default pyexasol resolves host names to IP addresses, randomly shuffles the IP addresses and tries to connect until connection succeeds. This has the following reasons: - -* This will ensure that if at least one hostname is unavailable, an exception will be raised. Otherwise, an exception will occur only when "random" selects a broken hostname, leading to unpredictable errors in production. - -* When you have a very large cluster with a growing number of nodes, it makes sense to put all nodes under one hostname, like `myexasol.mlan`, instead of having separate hostnames like `myexasol1..64.mlan`, especially when the number constantly changes. In this case, redundancy will not work properly if the hostname is not resolved beforehand, as we do not know if it points to a single address or multiple addresses. - -* For redundancy, we do not want to try the same IP address twice. To our knowledge, this cannot be guaranteed if we do not connect by IP. diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md deleted file mode 100644 index 48c9e00..0000000 --- a/docs/DEVELOPER_GUIDE.md +++ /dev/null @@ -1,27 +0,0 @@ -# Developer Guide - -This guide explains how to develop `pyexasol` and run tests. - -## Initial Setup - -Create a virtual environment and install dependencies: - -```sh -poetry install --all-extras -``` - -Run the following to enter the virtual environment: - -```sh -poetry shell -``` - -## Running Integration Tests - -To run integration tests first start a local database: - -```sh -nox -s db-start -``` - -Then you can run tests as usual with `pytest`. diff --git a/docs/ENCRYPTION.md b/docs/ENCRYPTION.md deleted file mode 100644 index 1f75013..0000000 --- a/docs/ENCRYPTION.md +++ /dev/null @@ -1,65 +0,0 @@ -# Encryption - -Similar to other Exasol connectors, PyEXASOL is capable of using TLS cryptographic protocol. - -Exasol published a few articles describing the details: -- [Using TLS with our analytics database (part 1): understanding the basics](https://www.exasol.com/resource/using-tls-with-our-analytics-database-understanding-the-basics/); -- [Using TLS with our analytics database (part 2): secure communication with Exasol](https://www.exasol.com/resource/using-tls-with-our-analytics-database-secure-communication-with-exasol/) -- [TLS for all Exasol drivers](https://www.exasol.com/support/browse/EXASOL-2936) - -### Default - -Encryption is ENABLED by default starting from PyEXASOL version `0.24.0`. - -Encryption was DISABLED by default in previous versions. - -### Certification verification - -- Exasol running "on-premises" uses self-signed SSL certificate by default. You may generate a proper SSL certificate and upload it using [instruction](https://docs.exasol.com/administration/on-premise/access_management/tls_certificate.htm). -- Exasol Docker uses self-signed SSL certificate by default. You may generate a proper SSL certificate and use it via editing of EXAConf file. More details are available on the [GitHub page](https://github.com/exasol/docker-db). -- Exasol SAAS running in the cloud uses proper certificate generated by public certificate authority. It does not require any extra setup. - -Certificate verification is disabled by default for connections with username and password. Certificate verification is enabled by default for connections with username and OpenID token. - -Similar to JDBC / ODBC drivers, PyEXASOL supports fingerprint certificate verification. Please check the examples below. - -### Specific examples - -1) How to connect with TLS encryption: - -```python -pyexasol.connect(dsn='myexasol:8563' - , user='user' - , password='password') -``` - -2) How to connect with TLS encryption and fingerprint verification: - -```python -pyexasol.connect(dsn='myexasol/135a1d2dce102de866f58267521f4232153545a075dc85f8f7596f57e588a181:8563' - , user='user' - , password='password' - ) -``` - -3) How to connect with TLS encryption and full certificate verification "on-premises" using internal root CA (certificate authority): - -```python -pyexasol.connect(dsn='myexasol:8563' - , user='user' - , password='password' - , websocket_sslopt={ - "cert_reqs": ssl.CERT_REQUIRED, - "ca_certs": '/path/to/rootCA.crt', - }) -``` - -4) How to connect to Exasol SAAS (TLS encryption is REQUIRED for SAAS): - -```python -pyexasol.connect(dsn='abc.cloud.exasol.com:8563' - , user='user' - , refresh_token='token' - , encryption=True - ) -``` diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md deleted file mode 100644 index b5eb8bc..0000000 --- a/docs/EXAMPLES.md +++ /dev/null @@ -1,65 +0,0 @@ -## Preparation -Basic preparation steps are required to see examples in action. - -1. Install PyEXASOL with [optional dependencies](/docs/DEPENDENCIES.md). -2. Download [PyEXASOL source code](https://github.com/exasol/pyexasol/archive/master.zip) and unzip it. -3. Make sure Exasol is installed and dedicated schema for testing is created. You may use free [Exasol Community Edition](https://www.exasol.com/portal/display/DOWNLOAD/Free+Trial) for testing purposes. -4. Open `/examples/` directory and edit file `\_config.py`. Input your Exasol credentials. -5. Run script to prepare data set for testing: -``` -python examples/a00_prepare.py -``` - -That's all. Now you may run examples in any order like common python scripts. E.g.: -``` -python examples/a01_basic.py -``` - -### Examples of core functions - -- [a01_basic.py](/examples/a01_basic.py) - minimal code to create connection and run query; -- [a02_fetch_tuple.py](/examples/a02_fetch_tuple.py) - all methods of fetching result set returning tuples; -- [a03_fetch_dict.py](/examples/a03_fetch_dict.py) - all methods of fetching result set returning dictionaries; -- [a04_fetch_mapper.py](/examples/a04_fetch_mapper.py) - apply custom data type mapper for fetching; -- [a05_formatting.py](/examples/a05_formatting.py) - SQL text [formatting](/docs/SQL_FORMATTING.md); -- [a06_transaction.py](/examples/a06_transaction.py) - transaction management, autocommit; -- [a07_exceptions.py](/examples/a07_exceptions.py) - error handling for basic SQL queries; -- [a08_ext.py](/examples/a08_ext.py) - extension functions to help with common problems outside of scope of database driver; -- [a09_abort_query.py](/examples/a09_abort_query.py) - abort running query from separate thread; -- [a10_context_manager.py](/examples/a10_context_manager.py) - use WITH clause for `ExaConnection` and `ExaStatement` objects; -- [a11_insert_multi](/examples/a11_insert_multi.py) - INSERT small number of rows using prepared statements instead of HTTP transport; -- [a12_meta](/examples/a12_meta.py) - lock-free meta data requests; -- [a13_meta_nosql](/examples/a13_meta_nosql.py) - no-SQL metadata commands introduces in Exasol v7.0+; - -### Examples of HTTP transport - -- [b01_pandas.py](/examples/b01_pandas.py) - IMPORT / EXPORT to and from `pandas.DataFrame`; -- [b02_import_export.py](/examples/b02_import_export.py) - other methods of IMPORT / EXPORT; -- [b03_parallel_export.py](/examples/b03_parallel_export.py) - multi-process HTTP transport for EXPORT; -- [b04_parallel_import.py](/examples/b04_parallel_import.py) - multi-process HTTP transport for IMPORT; -- [b05_parallel_export_import.py](/examples/b05_parallel_export_import.py) - multi-process HTTP transport for EXPORT followed by IMPORT; -- [b06_http_transport_errors](/examples/b06_http_transport_errors.py) - various ways to break HTTP transport and handle errors; - -## Examples of misc functions - -- [c01_redundancy.py](/examples/c01_redundancy.py) - connection redundancy, handling of missing nodes; -- [c02_edge_case.py](/examples/c02_edge_case.py) - storing and fetching biggest and smallest values for data types available in Exasol; -- [c03_db2_compat.py](/examples/c03_db2_compat.py) - [DB-API 2.0 compatibility wrapper](/docs/DBAPI_COMPAT.md); -- [c04_encryption.py](/examples/c04_encryption.py) - SSL-encrypted WebSocket connection and HTTP transport; -- [c05_session_params.py](/examples/c05_session_params.py) - passing custom session parameters `client_name`, `client_version`, etc.; -- [c06_local_config.py](/examples/c06_local_config.py) - connect using local config file; -- [c07_profiling.py](/examples/c07_profiling.py) - last query profiling; -- [c08_snapshot_transactions.py](/examples/c08_snapshot_transactions.py) - snapshot transactions mode, which may help with metadata locking problems; -- [c09_script_output.py](/examples/c09_script_output.py) - run query with UDF script and capture output (may not work on local laptop); -- [c10_overload.py](/examples/c10_overload.py) - extend core PyEXASOL classes to add custom logic; -- [c11_quote_ident.py](/examples/c11_quote_ident.py) - enable quoted identifiers for `import_*`, `export_*` and other relevant functions; -- [c12_thread_safety.py](/examples/c12_thread_safety.py) - built-in protection from accessing connection object from multiple threads simultaneously; -- [c13_dsn_parsing.py](/examples/c13_dsn_parsing.py) - parsing of complex connection strings and catching relevant exceptions; -- [c14_http_proxy.py](/examples/c14_http_proxy.py) - connection via HTTP proxy; -- [c15_garbage_collection](/examples/c15_garbage_collection.py) - detect potential garbage collection problems due to cross-references; - -## Examples of JSON libraries used for fetching - -- [j01_rapidjson.py](/examples/j01_rapidjson.py) - JSON library `rapidjson`; -- [j02_ujson.py](/examples/j02_ujson.py) - JSON library `ujson`; -- [j03_orjson.py](/examples/j03_orjson.py) - JSON library `orjson`; diff --git a/docs/HTTP_TRANSPORT.md b/docs/HTTP_TRANSPORT.md deleted file mode 100644 index d24dc0b..0000000 --- a/docs/HTTP_TRANSPORT.md +++ /dev/null @@ -1,163 +0,0 @@ -# HTTP transport - -The main purpose of HTTP transport is to reduce massive fetching overhead associated with large data sets (1M+ rows). It uses native Exasol commands `EXPORT` and `IMPORT` specifically designed to move large amounts of data. Data is transferred using CSV format with optional zlib compression. - -This is a powerful tool which helps to bypass creation of intermediate Python objects altogether and dramatically increases performance. - -PyEXASOL offloads HTTP communication and decompression to separate thread using [threading](https://docs.python.org/3/library/threading.html) module. Main thread deals with a simple [pipe](https://docs.python.org/3/library/os.html#os.pipe) opened in binary mode. - -You may specify a custom `callback` function to read or write from raw pipe and to apply complex logic. Use `callback_params` to pass additional parameters to `callback` function (e.g. options for pandas). - -You may also specify `import_params` or `export_params` to alter `IMPORT` or `EXPORT` query and modify CSV data stream. - -# Pre-defined functions - -## Export from Exasol to pandas -Export data from Exasol into `pandas.DataFrame`. You may use `callback_param` argument to pass custom options for pandas [`read_csv`](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.read_csv.html) function. - -```python -# Read from SQL -pd = C.export_to_pandas("SELECT * FROM users") - -# Read from table -pd = C.export_to_pandas("users") -``` - -## Import from pandas to Exasol -Import data from `pandas.DataFrame` into Exasol table. You may use `callback_param` argument to pass custom options for pandas [`to_csv`](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.to_csv.html) function. - -```python -C.import_from_pandas(pd, "users") -``` - -## Import from list (a-la INSERT) - -```python -my_list = [ - (1, 'Bob', False, '2018-01-01'), - (2, 'Gill', True, '2018-02-01'), -] - -C.import_from_iterable(my_list, "users") -``` - -## Import from generator -This function is suitable for very big INSERTS as long as generator returns rows 1-by-1 and does not run out of memory. - -```python -def my_generator(): - for i in range(5): - yield (i, 'Bob', True, '2017-01-01') - -C.import_from_iterable(my_generator(), "users") -``` - -## Import from file -Import data from file, path object or file-like object opened in binary mode. You may import from process `STDIN` using `sys.stdin.buffer`. -```python -# Import from file defined with string path -C.import_from_file('/test/my_file.csv', "users") - -# Import from path object -C.import_from_file(pathlib.Path('/test/my_file.csv'), "users") - -# Import from opened file -file = open('/test/my_file.csv', 'rb') -C.import_from_file(file, "users") -file.close() - -# Import from STDIN -C.import_from_file(sys.stdin.buffer, "users") -``` - -## Export to file -Export data from Exasol into file, path object or file-like object opened in binary mode. You may export to process `STDOUT` using `sys.stdout.buffer`. -```python -# Export from file defined with string path -C.export_to_file('my_file.csv', "users") - -# Export into STDOUT -C.export_to_file(sys.stdout.buffer, "users") -``` - -# Parameters - -Please refer to Exasol User Manual to know more about `IMPORT` / `EXPORT` parameters. - -### import_params -| Name | Example | Description | -| --- | --- | --- | -| `column_separator` | `,` | Column separator for CSV | -| `column_delimiter` | `"` | Column delimiter for CSV (quotting) | -| `columns` | `['id', 'name']` | List of table columns in data source, useful if column order of data source does not match column order of Exasol table | -| `csv_cols` | `["1..5", "6 FORMAT='999.99'", "8"]` | List of CSV columns with optional [numeric](https://docs.exasol.com/sql_references/formatmodels.htm#NumericFormat) or [date](https://docs.exasol.com/sql_references/formatmodels.htm#DateTimeFormat) formats | -| `row_separator` | `LF` | Row separator for CSV (line-endings) | -| `encoding` | `UTF8` | File encoding | -| `with_column_names` | `True` | Add column names as first line, useful for Pandas | -| `null` | `\N` | Custom `NULL` value | -| `delimit` | `AUTO` | Delimiter mode: `AUTO`, `ALWAYS`, `NEVER` | -| `format` | `gz` | Import file or stream compressed with `gz`, `bzip2`, `zip` | -| `comment` | `This is a query description` | Add a comment before the beginning of the query | - -### export_params -| Name | Example | Description | -| --- | --- | --- | -| `column_separator` | `,` | Column separator for CSV | -| `column_delimiter` | `"` | Column delimiter for CSV (quotting) | -| `columns` | `['id', 'name']` | List of table columns, useful to reorder table columns during export from table | -| `csv_cols` | `["1..5", "6 FORMAT='999.99'", "8"]` | List of CSV columns with optional [numeric](https://docs.exasol.com/sql_references/formatmodels.htm#NumericFormat) or [date](https://docs.exasol.com/sql_references/formatmodels.htm#DateTimeFormat) formats | -| `row_separator` | `LF` | Row separator for CSV (line-endings) | -| `encoding` | `UTF8` | File encoding | -| `skip` | `1` | How many first rows to skip, useful for skipping header | -| `null` | `\N` | Custom `NULL` value | -| `trim` | `TRIM` | Trim mode: `TRIM`, `RTRIM`, `LTRIM` | -| `format` | `gz` | Export file or stream compressed with `gz`, `bzip2`, `zip` | -| `comment` | `This is a query description` | Add a comment before the beginning of the query | - - -### The `comment` parameter, for adding comments to queries - -For any `export_*` or `import_*` call, you can add a comment that will be inserted before the beginning of the query. - -This can be used for profiling and auditing. Example: - -```python -C.import_from_file('/test/my_file.csv', 'users', import_params={'comment': ''' -This comment will be inserted before the query. -This query is importing user from CSV. -'''}) -``` - -The comment is inserted as a block comment (`/* */`). Block comment closing sequence (`*/`) is forbidden in the comment. - - -# How to write custom EXPORT \ IMPORT functions - -Full collection of pre-defined callback functions is available in [`callback.py`](/pyexasol/callback.py) module. - -Example of callback exporting into basic Python list. - -```python -# Define callback function -def export_to_list(pipe, dst, **kwargs): - wrapped_pipe = io.TextIOWrapper(pipe, newline='\n') - reader = csv.reader(wrapped_pipe, lineterminator='\n', **kwargs) - - return [row for row in reader] - -# Run EXPORT using defined callback function -C.export_to_callback(export_to_list, None, 'my_table') -``` - -Example of callback importing from Pandas into Exasol. - -```python -df = - -def import_from_pandas(pipe, src, **kwargs): - wrapped_pipe = io.TextIOWrapper(pipe, newline='\n') - return src.to_csv(wrapped_pipe, header=False, index=False, quoting=csv.QUOTE_NONNUMERIC, **kwargs) - -# Run IMPORT using defined callback function -C.export_from_callback(import_from_pandas, df, 'my_table') -``` diff --git a/docs/HTTP_TRANSPORT_PARALLEL.md b/docs/HTTP_TRANSPORT_PARALLEL.md deleted file mode 100644 index 9ab8271..0000000 --- a/docs/HTTP_TRANSPORT_PARALLEL.md +++ /dev/null @@ -1,37 +0,0 @@ -# HTTP transport (parallel) - -It is possible to run [HTTP Transport](/docs/HTTP_TRANSPORT.md) in parallel. Workload may be distributed across multiple CPU cores and even across multiple servers. - -## How it works on high level - -1. Parent process opens main connection to Exasol and spawns multiple child processes. -2. Each child process connects to individual Exasol node using [`http_transport()`](/docs/REFERENCE.md#http_transport), gets internal Exasol address (`ipaddr:port` string) using `.address` property, and sends it to parent process. -3. Parent process collects list of internal Exasol addresses from child processes and runs [`export_parallel()`](/docs/REFERENCE.md#export_parallel) or [`import_parallel()`](/docs/REFERENCE.md#import_parallel) function to execute SQL query. -4. Each child process runs callback function and reads or sends chunk of data from or to Exasol. -5. Parent process waits for SQL query and child processes to finish. - -![Parallel export](/docs/img/parallel_export.png) - -Please note that PyEXASOL does not provide any specific way to send internal Exasol address strings from child processes to parent process. You are free to choose your own way of inter-process communication. For example, you may use [multiprocessing.Pipe](https://docs.python.org/3/library/multiprocessing.html?highlight=Pipes#exchanging-objects-between-processes). - -## Examples - -- [b03_parallel_export](/examples/b03_parallel_export.py) for EXPORT; -- [b04_parallel_import](/examples/b04_parallel_import.py) for IMPORT; -- [b05_parallel_export_import](/examples/b05_parallel_export_import.py) for EXPORT followed by IMPORT using the same child processes; - -## Example of EXPORT query executed in Exasol - -This is how complete query looks from Exasol perspective. - -```sql -EXPORT my_table INTO CSV -AT 'http://27.1.0.30:33601' FILE '000.csv' -AT 'http://27.1.0.31:41733' FILE '001.csv' -AT 'http://27.1.0.32:45014' FILE '002.csv' -AT 'http://27.1.0.33:42071' FILE '003.csv' -AT 'http://27.1.0.34:36669' FILE '004.csv' -AT 'http://27.1.0.35:36794' FILE '005.csv' -WITH COLUMN HEADERS -; -``` diff --git a/docs/KNOWN_ISSUES.md b/docs/KNOWN_ISSUES.md deleted file mode 100644 index 655fab4..0000000 --- a/docs/KNOWN_ISSUES.md +++ /dev/null @@ -1,25 +0,0 @@ -# Known Issues - - -## `ssl.SSLError: Underlying socket connection gone (_ssl.c:...)` - -This error can occur in rare cases, for additional details see [GH-Issue #108](https://github.com/exasol/pyexasol/issues/108) -and `pyexasol.connection.ExaConnection:__del__`. -The root cause of this issue usually stems from a connection not being properly closed before program exit (interpreter shutdown). - - -Example Output: -```python -Exception ignored in: -Traceback (most recent call last): - File ".../test_case.py", line 14, in __del__ - File ".../lib/python3.10/site-packages/pyexasol/connection.py", line 456, in close - File ".../lib/python3.10/site-packages/pyexasol/connection.py", line 524, in req - File ".../lib/python3.10/site-packages/websocket/_core.py", line 285, in send - File ".../lib/python3.10/site-packages/websocket/_core.py", line 313, in send_frame - File ".../lib/python3.10/site-packages/websocket/_core.py", line 527, in _send - File ".../lib/python3.10/site-packages/websocket/_socket.py", line 172, in send - File ".../lib/python3.10/site-packages/websocket/_socket.py", line 149, in _send - File "/usr/lib/python3.10/ssl.py", line 1206, in send -ssl.SSLError: Underlying socket connection gone (_ssl.c:2326)` -``` \ No newline at end of file diff --git a/docs/LOCAL_CONFIG.md b/docs/LOCAL_CONFIG.md deleted file mode 100644 index cb280ed..0000000 --- a/docs/LOCAL_CONFIG.md +++ /dev/null @@ -1,35 +0,0 @@ -# Local config - -Local config is a popular feature among data scientists and analysts working on laptops. It allows users to store personal Exasol credentials and connection options in local file and separate it from code. - -In order to use local config, please create file `~/.pyexasol.ini` in your home directory. Here is the sample contents of this file: - -``` -[my_exasol] -dsn = myexasol1..5 -user = my_user -password = my_password -schema = my_schema -compression = True -fetch_dict = True - -``` - -You may specify any parameters available in [`connect`](/docs/REFERENCE.md#connect) function, except parameters expecting Python class or function. - -You may specify multiple sections. Each section represents separate connection config. It might be useful if you have multiple Exasol instances. - -In order to create connection using local config, please call function [`connect_local_config`](/docs/REFERENCE.md#connect_local_config). - -```python -import pyexasol - -C = pyexasol.connect_local_config('my_exasol') - -st = C.execute("SELECT CURRENT_TIMESTAMP") -print(st.fetchone()) -``` - -You may specify different location for local config file using `config_path` argument of `connect_local_config` function. - -You may overload class `ExaLocalConfig` or create your own implementation of local config if you want more sophisticated config management. diff --git a/docs/PARALLELISM.md b/docs/PARALLELISM.md deleted file mode 100644 index 2c705a6..0000000 --- a/docs/PARALLELISM.md +++ /dev/null @@ -1,28 +0,0 @@ -# Parallelism in PyEXASOL - -Fundamentals: - -- 1 Exasol session can run only 1 SQL query in parallel. -- 1 PyEXASOL connection equals 1 Exasol session. -- [`threadsafety`](https://www.python.org/dev/peps/pep-0249/#threadsafety) level of PyEXASOL is `1` (threads may share the module, but not connections). - -## Best practices - -In practice, it means you have two possible options to achieve parallelism: - -1. Start multiple independent processes using `multiprocessing`, `subprocess` or similar modules. Each process should open its own PyEXASOL connection **after start**. -2. Use [parallel HTTP transport](/docs/HTTP_TRANSPORT_PARALLEL.md) to run 1 SQL query, but read or write actual data using multiple processes. - -All other options are inefficient or prone to errors. - -## Known problems when trying to use other options - -- Re-using one PyEXASOL connection in multiple threads will cause an exception. -- Opening multiple PyEXASOL connections in multiple threads will work, but you will experience a bottleneck in data processing. Your script will be bound by 1 CPU core due to GIL. -- Re-using one PyEXASOL connection in multiple processes will fail due to SSL context going out of sync. - -# Parallelism limitations - -Normally Exasol server can only run 100 queries in parallel. But the practical limit is much lower. - -It is recommended to avoid running more than 20-30 queries in parallel to improve performance. If your system experiences sudden bursts of activity, it is recommended to add a basic "queue" or a "proxy" as a system in the middle between clients and Exasol server. It will help to spread the workload and reduce the complexity of resource management for Exasol server. Which will lead to better performance overall. diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md deleted file mode 100644 index e8d48c6..0000000 --- a/docs/PERFORMANCE.md +++ /dev/null @@ -1,67 +0,0 @@ -# Performance tests - -Performance of database drivers depends on many factors. Results may vary depending on hardware, network, settings and data set properties. I strongly suggest to make your own performance tests before making any important decisions. - -In this sample test I want to compare: - -- [PyODBC](https://github.com/mkleehammer/pyodbc) -- [TurbODBC](https://github.com/blue-yonder/turbodbc) -- PyEXASOL - -I use Badoo production Exasol cluster for testing: -- 20 nodes -- 800+ CPU cores with hyper-threading -- 14 Tb of RAM -- 10 Gb private network connections -- 1 Gb public network connections - -I run three different types of tests: - -- Fetching "low random" data set using server in the same data center -- Fetching "high random" data set using server in the same data center -- Fetching data set using local laptop behind VPN and Wifi network (slow network) - -I use default number of rows in test table: 10 millions of rows, mixed data types. - -I measure total rounded execution time in seconds using `time` command in bash. - -## Results - -| | [Low random](/performance/_low_random.log) | [High random](/performance/_high_random.log) | [Slow network](/performance/_remote.log) | -| --- | --- | --- | --- | -| [PyODBC fetchall](/performance/01_pyodbc_fetch.py) | 106 | 107 | - | -| [TurbODBC fetchall](/performance/02_turbodbc_fetch.py) | 56 | 55 | - | -| [PyEXASOL fetchall](/performance/03_pyexasol_fetch.py) | 32 | 39 | 126 | -| [PyEXASOL fetchall+zlib](/performance/03_pyexasol_fetch.py) | - | - | 92 | -| [TurbODBC fetchallnumpy](/performance/04_turbodbc_pandas_numpy.py) | 15 | 15 | - | -| [TurbODBC fetchallarrow](/performance/05_turbodbc_pandas_arrow.py) | 14 | 14 | - | -| [PyEXASOL export_to_pandas](/performance/06_pyexasol_pandas.py) | 11 | 21 | 77 | -| [PyEXASOL export_to_pandas+zlib](/performance/07_pyexasol_pandas_compress.py) | 28 | 53 | 29 | -| [PyEXASOL export_parallel](/performance/08_pyexasol_pandas_parallel.py) | 5 | 7 | - | - -### Conclusions - -1. PyODBC performance is trash (no surprise). -2. PyEXASOL standard fetching is faster than TurbODBC, but it happens mostly due to less ops with Python objects and due to zip() magic. -3. TurbODBC optimised fetching as numpy or arrow is very efficient and consistent, well done! -4. PyEXASOL export to pandas performance may vary depending on randomness of data set. It highly depends on pandas CSV reader. -5. PyEXASOL fetch and export with ZLIB compression is very good for slow network scenario, but it is bad for fast networks. -6. PyEXASOL parallel export beats everything else because it fully utilizes multiple CPU cores. - -## How to run your own test - -I strongly encourage you to run your own performance tests. You may use test scripts provided with PyEXASOL as the starting point. Make sure to use your production Exasol cluster for tests. Please do not use Exasol running in Docker locally, it eliminates the whole point of testing. - -1. Install PyODBC, TurbODBC, PyEXASOL, pandas. -2. Install Exasol ODBC driver. -3. Download [PyEXASOL source code](https://github.com/exasol/pyexasol/archive/master.zip) and unzip it. -4. Open `/performance/` directory and edit file `\_config.py`. Input your Exasol credentials, set table name and other settings. Set path to ODBC driver. -5. (optional) Run script to prepare data set for testing: -``` -python 00_prepare.py -``` - -That's all. Now you may run examples in any order like common python scripts. E.g.: -``` -time python 03_pyexasol_fetch.py -``` diff --git a/docs/PROTOCOL_VERSION.md b/docs/PROTOCOL_VERSION.md deleted file mode 100644 index 2507704..0000000 --- a/docs/PROTOCOL_VERSION.md +++ /dev/null @@ -1,27 +0,0 @@ -# WebSocket protocol versions - -## TL;DR (updated on 2021-09-27, since 0.21.0) - -If you have Exasol v6.1.9+, 6.2.5+ or v7+, do nothing. - -If you have an older Exasol version, and you get error message: - -> Could not create WebSocket protocol version - -please add connection option `protocol_version=pyexasol.PROTOCOL_V1` explicitly. - -## Explanation - -Exasol has the concept of "protocol version" which is used to extend the functionality of new database drivers without breaking the backwards compatibility with older database drivers. - -Exasol v6.x supports WebSocket protocol version [`1`](https://github.com/exasol/websocket-api/blob/master/docs/WebsocketAPIV1.md) only. - -Exasol v7.0+ supports WebSocket protocol versions [`1`](https://github.com/exasol/websocket-api/blob/master/docs/WebsocketAPIV1.md), [`2`](https://github.com/exasol/websocket-api/blob/master/docs/WebsocketAPIV2.md). - -Exasol v7.1+ supports WebSocket protocol versions [`1`](https://github.com/exasol/websocket-api/blob/master/docs/WebsocketAPIV1.md), [`2`](https://github.com/exasol/websocket-api/blob/master/docs/WebsocketAPIV2.md), [`3`](https://github.com/exasol/websocket-api/blob/master/docs/WebsocketAPIV3.md). - ---- - -When client (e.g. pyexasol) opens the connection with Exasol server, it sends the requested protocol version during the authorisation. Exasol server may or may not support the requested protocol version. If Exasol server does not support the requested version, it will downgrade the protocol version automatically. You may check the actual protocol version of connection using function [`.protocol_version()`](/docs/REFERENCE.md#protocol_version). - -However, the "downgrade" behaviour was introduced in the latest minor Exasol v6 versions only (`6.2.5`, `6.1.9`). All prior versions, including the whole `6.0.x` branch, will raise an exception if `protocol_version=2` or higher was requested. Please add connection option `protocol_version=1` explicitly if it happens. diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md deleted file mode 100644 index b7a0c8d..0000000 --- a/docs/REFERENCE.md +++ /dev/null @@ -1,785 +0,0 @@ -# Reference - -This page contains complete reference of PyEXASOL public API. - -- [connect()](#connect) -- [connect_local_config()](#connect_local_config) -- [http_transport()](#http_transport) -- [ExaConnection](#exaconnection) - - [execute()](#execute) - - [execute_udf_output()](#execute_udf_output) - - [commit()](#commit) - - [rollback()](#rollback) - - [set_autocommit()](#set_autocommit) - - [set_query_timeout()](#set_query_timeout) - - [open_schema()](#open_schema) - - [current_schema()](#current_schema) - - [export_to_file()](#export_to_file) - - [export_to_list()](#export_to_list) - - [export_to_pandas()](#export_to_pandas) - - [export_to_callback()](#export_to_callback) - - [export_parallel()](#export_parallel) - - [import_from_file()](#import_from_file) - - [import_from_iterable()](#import_from_iterable) - - [import_from_pandas()](#import_from_pandas) - - [import_from_callback()](#import_from_callback) - - [import_parallel()](#import_parallel) - - [get_nodes()](#get_nodes) - - [session_id()](#session_id) - - [protocol_version()](#protocol_version) - - [last_statement()](#last_statement) - - [abort_query()](#abort_query) - - [close()](#exaconnectionclose) - - [.attr](#attr) - - [.login_info](#login_info) - - [.options](#options) -- [ExaStatement](#exastatement) - - [\_\_iter\_\_()](#__iter__) - - [fetchone()](#fetchone) - - [fetchmany()](#fetchmany) - - [fetchall()](#fetchall) - - [fetchcol()](#fetchcol) - - [fetchval()](#fetchval) - - [rowcount()](#rowcount) - - [columns()](#columns) - - [column_names()](#column_names) - - [close()](#exastatementclose) - - [.execution_time](#execution_time) -- [ExaFormatter](#exaformatter) (.format) - - [format()](#format) - - [escape()](#escape) - - [escape_ident()](#escape_ident) - - [escape_like()](#escape_like) - - [quote()](#quote) - - [quote_ident()](#quote_ident) - - [safe_ident()](#safe_ident) - - [safe_float()](#safe_float) - - [safe_decimal()](#safe_decimal) -- [ExaMetaData](#exametadata) (.meta) - - [sql_columns()](#sql_columns) - - [schema_exists()](#schema_exists) - - [table_exists()](#table_exists) - - [view_exists()](#view_exists) - - [list_schemas()](#list_schemas) - - [list_tables()](#list_tables) - - [list_views()](#list_views) - - [list_columns()](#list_views) - - [list_objects()](#list_objects) - - [list_object_sizes()](#list_object_sizes) - - [list_sql_keywords()](#list_sql_keywords) - - [execute_snapshot()](#execute_snapshot) - - [execute_meta_nosql()](#execute_meta_nosql) (requires Exasol v7.0+) -- [ExaExtension](#exaextension) (.ext) - - [insert_multi()](#insert_multi) - - [get_disk_space_usage()](#get_disk_space_usage) - - [explain_last()](#explain_last) - - get_columns() (deprecated, please use [.meta.sql_columns()](#sql_columns)) - - get_columns_sql() (deprecated, please use [.meta.sql_columns()](#sql_columns)) - - get_sys_columns() (deprecated, please use [.meta.list_columns()](#list_columns)) - - get_sys_tables() (deprecated, please use [.meta.list_tables()](#list_tables)) - - get_sys_views() (deprecated, please use [.meta.list_views()](#list_views)) - - get_sys_schemas() (deprecated, please use [.meta.list_schemas()](#list_schemas)) - - get_reserved_words() (deprecated, please use [.meta.list_sql_keywords()](#list_sql_keywords)) -- [ExaHTTPTransportWrapper](#exahttptransportwrapper) - - [export_to_callback()](#exahttptransportwrapperexport_to_callback) - - [import_from_callback()](#exahttptransportwrapperimport_from_callback) - - [.address](#exahttptransportwrapperaddress) - -## connect() -Open new connection and return `ExaConnection` object. - -| Argument | Example | Description | -| --- | --- | --- | -| `dsn` | `exasolpool1..5.mlan:8563` `10.10.127.1..11:8564` | Connection string, same format as standard JDBC / ODBC drivers | -| `user` | `sys` | Username | -| `password` | `exasol` | Password | -| `schema` | `ingres` | Open schema after connection (Default: `''`, no schema) | -| `autocommit` | `True` | Enable autocommit on connection (Default: `True`) | -| `snapshot_transactions` | `None` | Explicitly enable or disable [snapshot transactions](/docs/SNAPSHOT_TRANSACTIONS.md) on connection (Default: `None`, database default) | -| `connection_timeout` | `10` | Socket timeout in seconds used to establish connection (Default: `10`) | -| `socket_timeout` | `20` | Socket timeout in seconds used for requests after connection was established (Default: `30`) | -| `query_timeout` | `0` | Maximum execution time of queries before automatic abort (Default: `0`, no timeout) | -| `compression` | `True` | Use zlib compression both for WebSocket and HTTP transport (Default: `False`) | -| `encryption` | `True` | Use [SSL encryption](/docs/ENCRYPTION.md) for WebSocket communication and HTTP transport (Default: `True`) | -| `fetch_dict` | `False` | Fetch result rows as dicts instead of tuples (Default: `False`) | -| `fetch_mapper` | `pyexasol.exasol_mapper` | Use custom mapper function to convert Exasol values into Python objects during fetching (Default: `None`) | -| `fetch_size_bytes` | `5 * 1024 * 1024` | Maximum size of data message for single fetch request in bytes (Default: 5Mb) | -| `lower_ident` | `False` | Automatically lowercase identifiers (table names, column names, etc.) returned from relevant functions (Default: `False`) | -| `quote_ident` | `False` | Add double quotes and escape identifiers passed to relevant functions (`export_*`, `import_*`, `ext.*`, etc.) (Default: `False`) | -| `json_lib` | `rapidjson` | Supported values: [`rapidjson`](https://github.com/python-rapidjson/python-rapidjson), [`ujson`](https://github.com/esnme/ultrajson), [`orjson`](https://github.com/ijl/orjson), [`json`](https://docs.python.org/3/library/json.html) (Default: `json`) | -| `verbose_error` | `True` | Display additional information when error occurs (Default: `True`) | -| `debug` | `False` | Output debug information for client-server communication and connection attempts to STDERR | -| `debug_logdir` | `/tmp/` | Store debug information into files in `debug_logdir` instead of outputting it to STDERR | -| `udf_output_bind_address` | `('localhost', 8580)` | Specific server address to bind TCP server for script output (Default: `('', 0)`) | -| `udf_output_connect_address` | `('udf_host', 8580)` | Specific SCRIPT_OUTPUT_ADDRESS value to connect from Exasol to UDF script output server (Default: inherited from TCP server) | -| `udf_output_dir` | `/tmp` | Path or path-like object pointing to directory for script output log files (Default: `tempfile.gettempdir()`) | -| `http_proxy` | `http://myproxy.com:3128` | HTTP proxy string in Linux [`http_proxy`](https://www.shellhacks.com/linux-proxy-server-settings-set-proxy-command-line/) format (Default: `None`) | -| `resolve_hostnames` | `False` | Explicitly resolve host names to IP addresses before connecting. Deactivating this will let the operating system resolve the host name (Default: `True`) | -| `client_name` | `MyClient` | Custom name of client application displayed in Exasol sessions tables (Default: `PyEXASOL`) | -| `client_version` | `1.0.0` | Custom version of client application (Default: `pyexasol.__version__`) | -| `client_os_username` | `john` | Custom OS username displayed in Exasol sessions table (Default: `getpass.getuser()`) | -| `protocol_version` | `pyexasol.PROTOCOL_V3` | Major [WebSocket protocol version](/docs/PROTOCOL_VERSION.md) requested for connection (Default: `pyexasol.PROTOCOL_V3`) | -| `websocket_sslopt` | `{'cert_reqs': ssl.CERT_NONE}` | Set custom [SSL options](https://github.com/websocket-client/websocket-client/blob/2222f2c49d71afd74fcda486e3dfd14399e647af/websocket/_http.py#L210-L272) for WebSocket client | -| `access_token` | `...` | OpenID access token to use for the login process | -| `refresh_token` | `...` | OpenID refresh token to use for the login process | - -### Host Name Resolution - -By default pyexasol resolves host names to IP addresses, randomly shuffles the IP addresses and tries to connect until connection succeeds. See the [design documentation](/docs/DESIGN.md#automatic-resolution-and-randomization-of-connection-addresses) for details. - -If host name resolution causes problems, you can deactivate it by specifying argument `resolve_hostnames=False`. This may be required when connecting through a proxy that allows connections only to defined host names. In all other cases we recommend to omit the argument. - -## connect_local_config() -Open new connection and return `ExaConnection` object using local .ini file (usually `~/.pyexasol.ini`) to read credentials and connection parameters. Please read [local config](/docs/LOCAL_CONFIG.md) page for more details. - -| Argument | Example | Description | -| --- | --- | --- | -| `config_section` | `my_exasol` | Name of section in config file | -| `config_path` | `/etc/pyexasol.ini` | Path to config file (Default: `~/.pyexasol.ini`) | -| `**kwargs` | - | All other arguments from [`connect`](#connect) method; `**kwargs` override values from config file | - -## http_transport() -Open new HTTP connection and return `ExaHTTPTransportWrapper` object. This function is a part of [parallel HTTP transport API](/docs/HTTP_TRANSPORT_PARALLEL.md). - -| Argument | Example | Description | -| --- | --- | --- | -| `ipaddr` | `10.17.1.10` | IP address of one of Exasol nodes received from [`get_nodes()`](#get_nodes) | -| `port` | `8563` | Port of one of Exasol nodes received from [`get_nodes()`](#get_nodes) | -| `compression` | `True` | Use zlib compression for HTTP transport, must be the same as `compression` of main connection (Default: `False`) | -| `encryption` | `True` | Use [SSL encryption](/docs/ENCRYPTION.md) for HTTP transport, must be the same as `encryption` of main connection (Default: `True`) | - -**Note:** this function was changed in PyEXASOL 0.22.0. Third argument `mode` (EXPORT / IMPORT) was removed, it is no longer needed. Please update your code accordingly. - -## ExaConnection - -Object of this class holds connection to Exasol, performs client-server communication and manages fast HTTP transport. All dependent objects have back-reference to parent `ExaConnection` object (`self.connection`). - -### execute() -Execute SQL statement with optional formatting. - -| Argument | Example | Description | -| --- |---------------------------------------------| --- | -| `query` | `SELECT * FROM {table!i} WHERE col1={col1}` | SQL query text, possibly with placeholders | -| `query_params` | `{'table': 'users', 'col1':'bar'}` | (optional) Values for placeholders | - -Return instance of `ExaStatement` - -### execute_udf_output() -Execute SQL statement with optional formatting. Capture [output](/docs/SCRIPT_OUTPUT.md) of UDF scripts. - -| Argument | Example | Description | -| --- |---------------------------------------------| --- | -| `query` | `SELECT * FROM {table!i} WHERE col1={col1}` | SQL query text, possibly with placeholders | -| `query_params` | `{'table': 'users', 'col1':'bar'}` | (optional) Values for placeholders | - -Return tuple with two elements: (1) instance of `ExaStatement` and (2) list of `Path` objects for script output log files. - -Exasol should be able to open connection to the machine where current script is running. It is usually OK in the same data centre, but it is normally not working if you try to run this function on local laptop. - -### commit() -Wrapper for query `COMMIT` - -### rollback() -Wrapper for query `ROLLBACK` - -### set_autocommit() -Set `False` to execute following statements in transaction. Set `True` to get back to automatic COMMIT after each statement. - -Autocommit is `True` by default because Exasol has to commit indexes and statistics objects even for pure SELECT statements. Lack of default COMMIT may lead to serious performance degradation. - -| Argument | Example | Description | -| --- | --- | --- | -| `val` | `False` | Autocommit mode | - -### set_query_timeout() -Set the maximum time in seconds for which a query can run before Exasol kills it automatically. Set value `0` to disable timeout. - -It is highly recommended to set timeout for UDF scripts to avoid potential infinite loops and very long transactions. - -| Argument | Example | Description | -| --- | --- | --- | -| `val` | `10` | Timeout value in seconds | - -### open_schema() -Wrapper for `OPEN SCHEMA` - -| Argument | Example | Description | -| --- | --- | --- | -| `schema` | `ingres` | Schema name | - -### current_schema() -Return name of currently opened schema. Return empty string if no schema was opened. - -### export_to_file() -Export large amount of data from Exasol to file or file-like object using fast HTTP transport. -File must be opened in binary mode. - -| Argument | Example | Description | -| --- | --- | --- | -| `dst` | `open(my_file, 'wb')` `/tmp/file.csv` | Path to file or file-like object | -| `query_or_table` | `SELECT * FROM table` `table` `(schema, table)` | SQL query or table for export | -| `query_params` | `{'table': 'users', 'col1':'bar'}` | (optional) Values for SQL query placeholders | -| `export_params` | `{'with_column_names': True}` | (optional) Custom parameters for EXPORT query | - -### export_to_list() -Export large amount of data from Exasol to basic Python `list` using fast HTTP transport. This function may run out of memory. - -| Argument | Example | Description | -| --- | --- | --- | -| `query_or_table` | `SELECT * FROM table` `table` `(schema, table)` | SQL query or table for export | -| `query_params` | `{'table': 'users', 'col1':'bar'}` | (optional) Values for SQL query placeholders | -| `export_params` | `{'encoding': 'LATIN1'}` | (optional) Custom parameters for EXPORT query | - -Return `list` of `tuples`. - -### export_to_pandas() -Export large amount of data from Exasol to `pandas.DataFrame`. This function may run out of memory. - -| Argument | Example | Description | -| --- | --- | --- | -| `query_or_table` | `SELECT * FROM table` `table` `(schema, table)` | SQL query or table for export | -| `query_params` | `{'table': 'users', 'col1':'bar'}` | (optional) Values for SQL query placeholders | -| `export_params` | `{'encoding': 'LATIN1'}` | (optional) Custom parameters for EXPORT query | - -Return instance of `pandas.DataFrame` - -### export_to_callback() -Export large amount of data to user-defined callback function - -| Argument | Example | Description | -| --- | --- | --- | -| `callback` | `def my_callback(pipe, dst, **kwargs)` | Callback function | -| `dst` | `anything` | (optional) Export destination for callback function | -| `query_or_table` | `SELECT * FROM table` `table` `(schema, table)` | SQL query or table for export | -| `query_params` | `{'table': 'users', 'col1':'bar'}` | (optional) Values for SQL query placeholders | -| `export_params` | `{'with_column_names': True}` | (optional) Custom parameters for EXPORT query | - -Return result of callback function - -### export_parallel() -This function is part of [parallel HTTP transport API](/docs/HTTP_TRANSPORT_PARALLEL.md). It accepts list of `ipaddr:port` strings obtained from all child processes and executes parallel export query. Parent process only monitors the execution of query. All actual work is performed in child processes. - -| Argument | Example | Description | -| --- | --- | --- | -| `exa_address_list` | `['27.0.1.10:5362', '27.0.1.11:7262']` | List of `ipaddr:port` strings obtained from HTTP transport [.address](#exahttptransportwrapperaddress) | -| `query_or_table` | `SELECT * FROM table` `table` `(schema, table)` | SQL query or table for export | -| `query_params` | `{'table': 'users', 'col1':'bar'}` | (optional) Values for SQL query placeholders | -| `export_params` | `{'with_column_names': True}` | (optional) Custom parameters for EXPORT query | - -Return nothing on successful export. You may access `EXPORT` statement results using [`last_statement()`](#last_statement) function. - -### import_from_file() -Import large amount of data from file or file-like object to Exasol. File must be opened in binary mode. - -| Argument | Example | Description | -| --- | --- | --- | -| `src` | `open(my_file, 'rb')` `/tmp/my_file.csv` | Source file or file-like object | -| `table` | `my_table` `(my_schema, my_table)` | Destination table for IMPORT | -| `import_params` | `{'column_separator: ','}` | (optional) Custom parameters for IMPORT query | - -### import_from_iterable() -Import large amount of data from `iterable` Python object to Exasol. Iterator must return tuples of values. - -| Argument | Example | Description | -| --- | --- | --- | -| `src` | `[(123, 'a')]` | Source object implementing `__iter__` | -| `table` | `my_table` `(my_schema, my_table)` | Destination table for IMPORT | -| `import_params` | `{'column_separator: ','}` | (optional) Custom parameters for IMPORT query | - -### import_from_pandas() -Import large amount of data from `pandas.DataFrame` to Exasol. - -| Argument | Example | Description | -| --- | --- | --- | -| `src` | `[(123, 'a')]` | Source `pandas.DataFrame` instance | -| `table` | `my_table` `(my_schema, my_table)` | Destination table for IMPORT | -| `import_params` | `{'column_separator: ','}` | (optional) Custom parameters for IMPORT query | - -### import_from_callback() -Import large amount of data from user-defined callback function to Exasol. - -| Argument | Example | Description | -| --- | --- | --- | -| `callback` | `def my_callback(pipe, src, **kwargs)` | Callback function | -| `src` | `anything` | Source for callback function | -| `table` | `my_table` `(my_schema, my_table)` | Destination table for IMPORT | -| `callback_params` | `{'a': 'b'}` | (optional) Dict with additional parameters for callback function | -| `import_params` | `{'column_separator': ','}` | (optional) Custom parameters for IMPORT query | - -### import_parallel() -This function is part of [parallel HTTP transport API](/docs/HTTP_TRANSPORT_PARALLEL.md). It accepts list of `ipaddr:port` strings obtained from all child processes and executes parallel import query. Parent process only monitors the execution of query. All actual work is performed in child processes. - -| Argument | Example | Description | -| --- | --- | --- | -| `exa_address_list` | `['27.0.1.10:5362', '27.0.1.11:7262']` | List of `ipaddr:port` strings obtained from HTTP transport [.address](#exahttptransportwrapperaddress) | -| `table` | `table` `(schema, table)` | Destination table for import | -| `import_params` | `{'column_separator': ','}` | (optional) Custom parameters for IMPORT query | - -Return nothing on successful export. You may access `IMPORT` statement results using [`last_statement()`](#last_statement) function. - -### get_nodes() - -Return list of currently active Exasol nodes which is normally used for [parallel HTTP transport](/docs/HTTP_TRANSPORT_PARALLEL.md). - -| Argument | Example | Description | -| --- | --- | --- | -| `pool_size` | `10` | (optional) Return list of specific size | - -Result format: `[{'ipaddr': , 'port': , 'idx': }]` - -If `pool_size` is bigger than number of nodes, list will wrap around and nodes will repeat with different `idx`. If `pool_size` is omitted, returns every active node once. - -Exasol shuffles list for every connection. - -### session_id() -Return unique `SESSION_ID` of the current session. Return value type is `str`. - -### protocol_version() -Return the actual protocol version of the established connection. Actual protocol version may be lower than requested protocol version defined by `protocol_version` connection option. - -The possible values are: `pyexasol.PROTOCOL_V1`, `pyexasol.PROTOCOL_V2`, `pyexasol.PROTOCOL_V3`. It may also return `0` if called before the connection was established, which is possible during the exception handling. - -You may read more about protocol versions [here](/docs/PROTOCOL_VERSION.md). - -### last_statement() -Get last `ExaStatement` object. It is useful while working with `export_*` and `import_*` functions normally returning result of callback function instead of statement object. - -Return instance of `ExaStatement`. - -### abort_query() -Abort running query. - -This function should be called from a separate thread and has no response. Response should be checked in the main thread which started execution of query. Please check [27_abort_query](/examples/a09_abort_query.py) example. - -There are three possible outcomes of calling this function: - -1) Query is aborted normally, connection remains active; -2) Query was stuck in a state which cannot be aborted, so Exasol has to terminate connection; -3) Query might be finished successfully before abort call had a chance to take effect; - -Please note that you may terminate the whole Python process to close WebSocket connection. It will stop running query automatically. - -### ExaConnection.close() -Closes connection to database. - -| Argument | Example | Description | -| --- | --- | --- | -| `disconnect` | `True` | (optional) Send [`disconnect`](https://github.com/exasol/websocket-api/blob/master/WebsocketAPI.md#disconnect-closes-a-connection-to-exasol) command before closing the WebSocket connection (default: `True`) | - -### .attr - -Read-only `dict` of attributes of current connection. The most notable attributes are: - -| Attribute | Description | -| --- | --- | -| `autocommit` | Current state of autocommit (enabled / disabled) | -| `queryTimeout` | Current state of query timeout measured in seconds (0 = disabled) | -| `snapshotTransactionsEnabled` | Current state of [snapshot transactions](/docs/SNAPSHOT_TRANSACTIONS.md) mode (enabled / disabled) | -| `timezone` | Timezone of the current session | - -Full list of possible attributes is available [here](https://github.com/exasol/websocket-api/blob/master/WebsocketAPI.md#attributes-session-and-database-properties). - -### .login_info - -Read-only `dict` of login information returned by second response of LOGIN command. The most notable info key are: - -| Info | Description | -| --- | --- | -| `sessionId` | Unique `SESSION_ID` of current connection. It is advisable to use [`session_id()`](#session_id) wrapper function to get this info. | -| `protocolVersion` | WebSocket protocol version actually used for connection. It may be lower than requested `protocol_version` in connection arguments. | -| `releaseVersion` | Version of Exasol (e.g. `6.0.15`) | -| `databaseName` | Name of Exasol instance | - -Full list of possible keys is available [here](https://github.com/exasol/websocket-api/blob/master/WebsocketAPI.md#login-establishes-a-connection-to-exasol). Please scroll down a bit to find "responseData". - -### .options - -Read-only `dict` of arguments passed to [`connect()`](#connect) function and used to create ExaConnection object. - -## ExaStatement - -Object of this class executes and helps to fetch result set of single Exasol SQL statement. Unlike typical `Cursor` object, `ExaStatement` is not reusable. - -`ExaStatement` may fetch result set rows as `tuples` (default) or as `dict` (set `fetch_dict=True` in connection options). - -`ExaStatement` may use custom data-type mapper during fetching (set `fetch_mapper=` in connection options). Mapper function accepts two arguments (raw `value` and `dataType` object) and returns custom object or value. - -`ExaStatement` fetches big result sets in chunks. The size of chunk may be adjusted (set `fetch_size_bytes=` in connection options). - -### \_\_iter\_\_() -The best way to fetch result set of statement is to use iterator: - -```python -st = pyexasol.execute('SELECT * FROM table') - -for row in st: - print(row) -``` - -Iterator yields `tuple` or `dict` depending on `fetch_dict` connection option. - -### fetchone() -Fetches one row. - -Return `tuple` or `dict`. Return `None` if all rows were fetched. - -### fetchmany() -Fetches multiple rows. - -| Argument | Example | Description | -| --- | --- | --- | -| `size` | `100` | Set the specific amount of rows to fetch (Default: `10000`) | - -Return `list` of `tuples` or `list` of `dict`. Return empty `list` if all rows were fetched previously. - -### fetchall() -Fetches all remaining rows. This function may run out of memory. - -Return `list` of `tuples` or `list` of `dict`. Return empty `list` if all rows were fetched previously. - -### fetchcol() -Fetches all values from first column. - -Return `list` of values. Return empty `list` if all rows were fetched previously. - -### fetchval() -Fetches first column of first row. It may be useful for queries returning single value like `SELECT count(*) FROM table`. - -Return value. Return `None` if all rows were fetched previously. - -### rowcount() -Depending on the type of query: - -- Return total amount of selected rows for statements with result set (`num_rows`). -- Return total amount of processed rows for DML queries (`row_count`). - -### columns() -Return `dict` with keys as `column names` and values as `dataType` objects defined in Exasol WebSocket protocol. - -| Names | Type | Description | -| --- | --- | --- | -| type | string | column data type | -| precision | number | (optional) column precision | -| scale | number | (optional) column scale | -| size | number | (optional) maximum size in bytes of a column value | -| characterSet | string | (optional) character encoding of a text column | -| withLocalTimeZone | true, false | (optional) specifies if a timestamp has a local time zone | -| fraction | number | (optional) fractional part of number | -| srid | number | (optional) spatial reference system identifier | - -Since the minimum supported version of Python is 3.6, the order of `dict` preserves the order of columns in result set. - -### column_names() - -Return `list` of column names. - -### ExaStatement.close() -Closes result set handle if it was opened. You won't be able to fetch next chunk of large dataset after calling this function, but no other side-effects. - -### .execution_time - -Execution time of SQL statement. It is measured by wall-clock time of WebSocket request, so real execution time is a bit faster. Return `float`. - - -## ExaFormatter - -`ExaFormatter` inherits standard Python `string.Formatter`. It introduces set of placeholders to prevent SQL injections specifically in Exasol dynamic SQL queries. It also completely disabled `format_spec` section of standard formatting since it has no use in context of SQL queries and may cause more harm than good. - -You may access these functions using `.format` property of connection object. Example: - -```python -C = pyexasol.connect(...) -print(C.format.escape('abc')) -``` - -### format() -Formats SQL query using given arguments. Definition is the same as standard `format` function. - -### escape() -Accepts raw value. Converts it to `str` and replaces `'` (single-quote) with `''` (two single-quotes). May be useful on its own when escaping small parts of bigger values. - -### escape_ident() -Accepts raw identifier. Converts it to `str` and replaces `"` (double-quote) with `""` (two double-quotes). May be useful on its own when escaping small parts of big identifiers. - -### escape_like() -Accepts raw value. Converts it to `str` and escapes for LIKE-pattern value. - -### quote() -Accepts raw value. Converts it to `str`, escapes it using `escape()` and wraps in `'` (single-quote). This is the primary function to pass arbitrary values to Exasol queries. - -### quote_ident() -Accepts raw identifier. Coverts it to `str`, escapes it using `escape_ident()` and wraps in `"` (double-quote). This is the primary function to pass arbitrary identifiers to Exasol queries. - -Also, accepts tuple of raw identifiers, applies `quote_ident` to all of them and joins with `.` (dot). It may be useful when referencing to `(schema, table)` or `(schema, table, column_name)`. - -Please note that identifiers in Exasol are upper-cased by default. If you pass lower-cased identifier into this function, Exasol will try to find object with lower-cased name and may fail. Please consider using `safe_ident()` function if want more convenience. - -### safe_ident() -Accepts raw identifier. Converts it to `str` and validates it. Then puts it into SQL query without any quotting. If passed values is not a valid identifier (e.g. contains spaces), throws `ValueError` exception. - -Also, accepts tuple of raw identifiers, validates all of them and joins with `.` (dot). - -It is the convenient version of `quote_ident` with softer approach to lower-cased identifiers. - -### safe_float() -Accepts raw value. Converts it to `str` and validates it as float value for Exasol. For example `+infinity`, `-infinity` are not valid Exasol values. If value is not valid, throws `ValueError` exception. - -### safe_decimal() -Accepts raw values. Converts it to `str` and validates it as decimal valie for Exasol. If value is not valid, throws `ValueError` exception. - -## ExaMetaData - -`ExaMetaData` provides convenient functions to perform lock-free meta data requests using `/*snapshot execution*/` SQL hint and prepared statements. If you still get locks, please make sure to update Exasol server to the latest minor version. - -You may access these functions using `.meta` property of connection object. Example: - -```python -C = pyexasol.connect(...) -print(C.meta.sql_columns('SELECT 1 AS id')) -``` - -### sql_columns - -Return columns of SQL query result without executing it. Output format is similar to [ExaStatement.columns()](#columns). - -| Argument | Example | Description | -| --- |---------------------------------------------| --- | -| `query` | `SELECT * FROM {table!i} WHERE col1={col1}` | SQL query text, possibly with placeholders | -| `query_params` | `{'table': 'users', 'col1':'bar'}` | (optional) Values for placeholders | - - -### schema_exists() - -Return `True` if schema exists, `False` otherwise. - -| Argument | Example | Description | -| --- | --- | --- | -| `schema_name` | `FINANCE` | Schema name | - -### table_exists() - -Return `True` if table exists, `False` otherwise. If schema name was not specified, [current_schema](#current_schema) will be used instead. - -| Argument | Example | Description | -| --- | --- | --- | -| `table_name` | `my_table`, `(my_schema, my_table)` | Table name (with optional schema name) | - -### view_exists() - -Return `True` if view exists, `False` otherwise. If schema name was not specified, [current_schema](#current_schema) will be used instead. - -| Argument | Example | Description | -| --- | --- | --- | -| `view_name` | `my_view`, `(my_schema, my_view)` | View name (with optional schema name) | - -### list_schemas() - -Return list of schemas from [EXA_SCHEMAS](https://docs.exasol.com/db/latest/sql_references/system_tables/metadata/exa_schemas.htm) system view matching LIKE-pattern. - -| Argument | Example | Description | -| --- | --- | --- | -| `schema_name_pattern` | `FINANCE`, `TEST%` | Schema name LIKE-pattern | - -Patterns are case-sensitive. You may escape LIKE-patterns using [.format.escape_like()](#escape_like). Response contains all columns from system view and might change depending on Exasol server version. - -### list_tables() - -Return list of tables from [EXA_ALL_TABLES](https://docs.exasol.com/db/latest/sql_references/system_tables/metadata/exa_all_tables.htm) system view matching LIKE-pattern. - -| Argument | Example | Description | -| --- | --- | --- | -| `table_schema_pattern` | `FINANCE`, `TEST%` | Schema name LIKE-pattern | -| `table_name_pattern` | `MY_TABLE`, `PAYMENTS_%` | Table name LIKE-pattern | - -Patterns are case-sensitive. You may escape LIKE-patterns using [.format.escape_like()](#escape_like). Response contains all columns from system view and might change depending on Exasol server version. - -### list_views() - -Return list of views from [EXA_ALL_VIEWS](https://docs.exasol.com/db/latest/sql_references/system_tables/metadata/exa_all_views.htm) system view matching LIKE-pattern. - -| Argument | Example | Description | -| --- | --- | --- | -| `view_schema_pattern` | `FINANCE`, `TEST%` | Schema name LIKE-pattern | -| `view_name_pattern` | `MY_VIEW`, `PAYMENTS_VIEW_%` | View name LIKE-pattern | - -Patterns are case-sensitive. You may escape LIKE-patterns using [.format.escape_like()](#escape_like). Response contains all columns from system view and might change depending on Exasol server version. - -### list_columns() - -Return list of columns from [EXA_ALL_COLUMNS](https://docs.exasol.com/db/latest/sql_references/system_tables/metadata/exa_all_columns.htm) system view matching LIKE-pattern. - -| Argument | Example | Description | -| --- | --- | --- | -| `column_schema_pattern` | `FINANCE`, `TEST%` | Schema name LIKE-pattern | -| `column_table_pattern` | `MY_VIEW`, `PAYMENTS_VIEW_%` | Object name LIKE-pattern | -| `column_table_type_pattern` | `TABLE`, `VIEW` | Object type LIKE-pattern | -| `column_name_pattern` | `USER_ID`, `USER_ID%` | Column name LIKE-pattern | - -Patterns are case-sensitive. You may escape LIKE-patterns using [.format.escape_like()](#escape_like). Response contains all columns from system view and might change depending on Exasol server version. - -### list_objects() - -Return list of objects from [EXA_ALL_OBJECTS](https://docs.exasol.com/db/latest/sql_references/system_tables/metadata/exa_all_objects.htm) system view matching LIKE-pattern. - -| Argument | Example | Description | -| --- | --- | --- | -| `object_name_pattern` | `MY_VIEW`, `PAYMENTS_VIEW_%` | Object name LIKE-pattern | -| `object_type_pattern` | `TABLE`, `VIEW`, `FUNCTION` | Object type LIKE-pattern | -| `owner_pattern` | `INGRES`, `SYS` | Owner (user or role) LIKE-pattern | -| `root_name_pattern` | `FINANCE`, `TEST%` | Root name LIKE-pattern, it normally refers to schema name | - -Patterns are case-sensitive. You may escape LIKE-patterns using [.format.escape_like()](#escape_like). Response contains all columns from system view and might change depending on Exasol server version. - -### list_object_sizes() - -Return list of objects with sizes from [EXA_ALL_OBJECT_SIZES](https://docs.exasol.com/db/latest/sql_references/system_tables/metadata/exa_all_object_sizes.htm) system view matching LIKE-pattern. - -Please note: object sizes do not include indices and statistics! - -| Argument | Example | Description | -| --- | --- | --- | -| `object_name_pattern` | `MY_VIEW`, `PAYMENTS_VIEW_%` | Object name LIKE-pattern | -| `object_type_pattern` | `TABLE`, `VIEW`, `FUNCTION` | Object type LIKE-pattern | -| `owner_pattern` | `INGRES`, `SYS` | Owner (user or role) LIKE-pattern | -| `root_name_pattern` | `FINANCE`, `TEST%` | Root name LIKE-pattern, it normally refers to schema name | - -Patterns are case-sensitive. You may escape LIKE-patterns using [.format.escape_like()](#escape_like). Response contains all columns from system view and might change depending on Exasol server version. - -### list_indices() - -Return list of indices with sizes from [EXA_ALL_INDICES](https://docs.exasol.com/db/latest/sql_references/system_tables/metadata/exa_all_indices.htm) system view matching LIKE-pattern. - -| Argument | Example | Description | -| --- | --- | --- | -| `index_schema_pattern` | `FINANCE`, `TEST%` | Schema name LIKE-pattern | -| `index_table_pattern` | `TABLE`, `VIEW`, `FUNCTION` | Table name LIKE-pattern | -| `index_owner_pattern` | `INGRES`, `SYS` | Owner (user or role) LIKE-pattern | - -Patterns are case-sensitive. You may escape LIKE-patterns using [.format.escape_like()](#escape_like). Response contains all columns from system view and might change depending on Exasol server version. - -### list_sql_keywords() - -Return list of SQL keywords from [EXA_SQL_KEYWORDS](https://docs.exasol.com/db/latest/sql_references/system_tables/metadata/exa_sql_keywords.htm) system view. - -These keywords cannot be used as identifiers without double quotes. - -Please try to avoid hardcoding this list. It might change depending on Exasol server version without warning. - -### execute_snapshot() - -Execute SQL statement in [snapshot execution](/docs/SNAPSHOT_TRANSACTIONS.md) mode. It prevents read locks and works for system tables and views only. - -Please do not try to query normal tables with this method. It will fail during creation of indices or statistics objects. - -| Argument | Example | Description | -| --- |---------------------------------------------| --- | -| `query` | `SELECT * FROM {table!i} WHERE col1={col1}` | SQL query text, possibly with placeholders | -| `query_params` | `{'table': 'users', 'col1':'bar'}` | (optional) Values for placeholders | - -Return instance of `ExaStatement` - -### execute_meta_nosql() - -Execute no SQL metadata command introduced in Exasol 7.0, [WebSocket protocol version 2](/docs/PROTOCOL_VERSION.md). - -The full list of metadata commands and arguments is available in the [official documentation](https://github.com/exasol/websocket-api/blob/master/docs/WebsocketAPIV2.md#metadata-related-commands). - -| Argument | Example | Description | -| --- | --- | --- | -| `meta_command` | `getTables` | Metadata command | -| `meta_params` | `{'schema': 'PYEXASOL%', 'table': 'USER%', 'tableTypes': ['TABLE', 'VIEW']}` | (optional) Parameters for metadata commands | - -Return instance of `ExaStatement` - -## ExaExtension - -This class provides additional capabilities to solve common Exasol-related problems which are normally out of scope of simple SQL driver. You should call `ExaConnection.ext` property in order to use those functions. - -You may access these functions using `.ext` property of connection object. Example: - -```python -C = pyexasol.connect(...) -print(C.ext.get_disk_space_usage()) -``` - -### insert_multi() - -INSERT small number of rows into table using prepared statement. It provides better performance for **small data sets of 10,000 rows or less** compared to [`import_from_iterable()`](#import_from_iterable). - -Please use [`import_from_iterable()`](#import_from_iterable) for larger data sets and better memory efficiency. Please use [`import_from_pandas()`](#import_from_pandas) to import from data frame regardless of its size. - -You may use `columns` argument to specify custom order of columns for insertion. If some columns are not included in this list, NULL or DEFAULT value will be used instead. - -| Argument | Example | Description | -| --- | --- | --- | -| `table_name` | `my_table` `(my_schema, my_table)` | Target table for INSERT | -| `data` | `[(1, 'foo'), (2, 'bar')]` | Source object implementing `__iter__` (e.g.: list of tuples) | -| `columns` | `['id', 'name']` | List of column names to specify custom order of columns | - -Please note that data should be presented in a row format. You may use `zip(*data_cols)` to convert columnar format into row format. - -### get_disk_space_usage() - -There is no easy-to-use Exasol function to get current disk usage. PyEXASOL tries to mitigate it and estimate this value using hidden system view. We take redundancy and free disk space into account. - -This function returns dictionary with 4 keys: - -| Key | Description | -| --- | --- | -| `occupied_size` | How much space is occupied (in bytes) | -| `free_size` | How much space is available (in bytes) | -| `total_size` | occupied_size + free_size | -| `occupied_size_percent` | Percentage of occupied disk space (0-100%) | - -### explain_last() - -Profile (EXPLAIN) last executed query. Example: [22_profiling](/examples/c07_profiling.py) - -| Argument | Example | Description | -| --- | --- | --- | -| `details` | `True` | (optional) return additional details | - -- `details=False` returns AVG or MAX values for all Exasol nodes. -- `details=True` returns separate rows for each individual Exasol node (column `iproc`). - -Details are useful to detect bad data distribution and imbalanced execution across multiple nodes. - -`COMMIT`, `ROLLBACK` and `FLUSH STATISTICS` queries are ignored. - -If you want to see real values of CPU, MEM, HDD, NET columns, please enable Exasol profiling first with: `ALTER SESSION SET PROFILE = 'ON';` - -Please refer to Exasol User Manuals for explanations about profiling columns. - -## ExaHTTPTransportWrapper - -Wrapper for [parallel HTTP transport](/docs/HTTP_TRANSPORT_PARALLEL.md) used by child processes. - -You may create this wrapper using [http_transport()](#http_transport) function. - -### ExaHTTPTransportWrapper.address - -Return internal Exasol address as `ipaddr:port` string. This string should be passed from child processes to parent process and used as an argument for [`export_parallel()`](#export_parallel) and [`import_parallel()`](#import_parallel) functions. - -### ExaHTTPTransportWrapper.export_to_callback() - -Exports chunk of data using callback function. You may use exactly the same callbacks utilized by standard non-parallel [`export_to_callback()`](#export_to_callback) function. - -| Argument | Example | Description | -| --- | --- | --- | -| `callback` | `def my_callback(pipe, dst, **kwargs)` | Callback function | -| `dst` | `anything` | (optional) Export destination for callback function | -| `callback_params` | `{'a': 'b'}` | (optional) Dict with additional parameters for callback function | - -Return result of callback function - -### ExaHTTPTransportWrapper.import_from_callback() - -Import chunk of data using callback function. You may use exactly the same callbacks utilized by standard non-parallel [`import_from_callback()`](#import_from_callback) function. - -| Argument | Example | Description | -| --- | --- | --- | -| `callback` | `def my_callback(pipe, dst, **kwargs)` | Callback function | -| `src` | `anything` | (optional) Import source for callback function | -| `callback_params` | `{'a': 'b'}` | (optional) Dict with additional parameters for callback function | - -Return result of callback function diff --git a/docs/SCRIPT_OUTPUT.md b/docs/SCRIPT_OUTPUT.md deleted file mode 100644 index df87fbc..0000000 --- a/docs/SCRIPT_OUTPUT.md +++ /dev/null @@ -1,61 +0,0 @@ -# UDF script output - -Exasol allows capturing combined output (STDOUT + STDERR) of UDF scripts. It is very helpful for debugging and to extract additional statistics from scripts running in production. - -In order to use this feature, user has to run TCP server and provide address via session parameters. For example: - -```sql -ALTER SESSION SET SCRIPT_OUTPUT_ADDRESS = 'myserver:16442'; -``` - -Exasol may run UDFs in parallel using large amount of VM's. Each VM opens individual connection to TCP Server and keeps it opened until the end of execution. TCP server must be prepared for large number of simultaneous connections. - -PyEXASOL provides such TCP server for your convenience. It can work in two different modes: - -### DEBUG MODE -Debug mode is useful for manual debugging during UDF script development. - -Connections are accepted from all VM's. Output of first connection is displayed. Outputs of other connections are discarded. - -Server runs forever until stopped by user. - -How to use it: - -1. Run server in debug mode; -``` -python -m pyexasol_utils.script_output -``` -2. Copy-paste provided SQL query to your SQL client and execute it; -3. Run queries with UDF scripts, see output in terminal; -4. Stop server with (Ctrl + C) when you finish debugging; - -Please note: if you have problems getting script output immediately out of VM's, please make sure you **flush** STDOUT / STDERR in your UDF script. Some programming languages (like Python) may buffer output by default. - -### SCRIPT MODE -Script mode is useful for production usage and during last stages of development. - -Connections are accepted from all VM's. Output of each VM is stored into separate log file. - -Server runs for single SQL statement and stops automatically. - -How to use it: -1. (optional) Create base directory for UDF script logs and set it using `udf_output_dir` connection option. -2. Execute query with UDF script using function [`execute_udf_output()`](/docs/REFERENCE.md#execute_udf_output). -3. Read and process files returned by function. - -Example: -```python -stmt, log_files = C.execute_udf_output("SELECT my_script(user_id) FROM table") - -printer.pprint(stmt.fetchall()) -printer.pprint(log_files[0].read_text()) - -``` - -You are responsible for deletion of log files. - -## Connectivity problem - -Unlike [HTTP Transport](/docs/HTTP_TRANSPORT.md), script output TCP server is real server. It receives incoming connections from Exasol nodes. Those connections might be blocked by firewalls and various network policies. You are responsible for making host with TCP server available for incoming connections. - -If you want to bind TCP server to specific address and port, you may use `--host`, `--port` arguments for debug mode and `udf_output_bind_address`, `udf_output_connect_address` connection options for script mode. diff --git a/docs/SNAPSHOT_TRANSACTIONS.md b/docs/SNAPSHOT_TRANSACTIONS.md deleted file mode 100644 index 7ffdefb..0000000 --- a/docs/SNAPSHOT_TRANSACTIONS.md +++ /dev/null @@ -1,27 +0,0 @@ -# Snapshot transactions - -At this moment Exasol supports only one transaction isolation level: SERIALIZABLE. - -This is good for data consistency, but it increases probability of transactions conflicts. You may read more about it here: - -- [Transaction System](https://exasol.my.site.com/s/article/Transaction-System?language=en_US) -- [WAIT FOR COMMIT on SELECT statement](https://exasol.my.site.com/s/article/WAIT-FOR-COMMIT-on-SELECT-statement?language=en_US) - -The most common locking problem is related to metadata selects from system views (tables, column, object sizes, etc.). JDBC and ODBC drivers provide special non-blocking calls for common metadata requests: getTables(), getColumns(). But there are no such calls for WebSocket drivers. - -The only way to access metadata in non-blocking manner with PyEXASOL is an internal feature called "Snapshot Transactions". Details are limited, but we managed to find out a few things: - -1. ExaPlus client uses snapshot transactions to access system views in separate META-session; -2. Snapshot transactions are read-only. Connection will crash instantly on any write attempt; -3. In this mode Exasol returns last snapshot of accessed objects instead of locking in `WAIT FOR COMMIT` state; - -### Recommended usage pattern - -If you want to read metadata without locks, and if strict transaction integrity is not an issue, please do the following: - -1. Open new connection with option `snapshot_transactions=True`. Use this connection to read metadata from system views only. -2. Open another connection in normal mode and use it for everything else. - -Follow this pattern and you should be fine. - -Please see [example_23](/examples/c08_snapshot_transactions.py) for common locking scenario solved by this feature. diff --git a/docs/SQL_FORMATTING.md b/docs/SQL_FORMATTING.md deleted file mode 100644 index e3a13f3..0000000 --- a/docs/SQL_FORMATTING.md +++ /dev/null @@ -1,93 +0,0 @@ -## SQL Formatting - -PyEXASOL provides custom Exasol-specific formatter based on standard [Python 3 Formatter](https://docs.python.org/3/library/string.html#string.Formatter). - -You're not forced to use this formatter. You can always overload it using `cls_formatter` connection option or format raw SQL yourself. - -## Types of placeholders - -Formatter supports only [new-style named placeholders](https://www.python.org/dev/peps/pep-3101/) and optional "conversion" to specify placeholder type. -``` -foo {a}, {b}, {c!s} -``` - -If type was not defined, formatter assumes it's a string value by default (`!s`). - -| Conversion | Function | Description | -| --- | --- | --- | -| `!s` | [`.quote()`](/docs/REFERENCE.md#quote) | Escapes string value and wraps it with single quotes. It can also be used for dates, timestamps, numbers, etc. | -| `!d` | [`.safe_decimal()`](/docs/REFERENCE.md#safe_decimal) | Validates decimal value and puts it without quotes. Could be useful for `LIMIT`, `OFFSET`, math expressions. | -| `!f` | [`.safe_float()`](/docs/REFERENCE.md#safe_float) | Similar to `!d`, but allows expressions with exponent part commonly used in float values. | -| `!i` | [`.safe_ident()`](/docs/REFERENCE.md#safe_ident) | Validates identifer and puts it without quotes. It allows you to pass lower-cased identifiers to query upper-cased named tables while keeping it "safe". It is the "convenient" version of identifier placeholder. | -| `!q` | [`.quote_ident()`](/docs/REFERENCE.md#quote_ident) | Escapes string identifier and wraps it with double quotes. It allows you to pass lower-cased identifiers to query lower-cased table names. It is the "proper" version of identifier placeholder. | -| `!r` | `str()` | Converts value to string and puts it "as is" without any escaping or checks. Useful as "raw sql" placeholder to build complex queries and to pass query parts like `ASC`, `DESC`, `TRUE`, `FALSE` etc. | - -All value-oriented placeholders convert `None` values into `NULL` without quotes. Please remember that Exasol empty string is converted to `NULL` by database itself. - -All identifier-oriented placeholder also accept `tuples` for multi-level identifier scenarios. For example: -```python -safe_ident('my_schema', 'my_table') ->>> my_schema.my_table - -quote_ident('my_schema', 'my_table', 'my_column') ->>> "my_schema"."my_table"."my_column" -``` - -For all `list`-typed values each element will be converted independently and joined into final string using `, ` (comma and space). You may use it to pass multiple values to `IN ()` and `NOT IN ()` expressions. - -## Complete example - -```python -# SQL with formatting -params = { - 'random_value': 'abc', - 'null_value': None, - 'table_name_1': 'users', - 'table_name_2': (config.schema, 'PAYMENTS'), - 'user_rating': '0.5', - 'user_score': 1e1, - 'is_female': 'TRUE', - 'user_statuses': ['ACTIVE', 'PASSIVE', 'SUSPENDED'], - 'exclude_user_score': [10, 20], - 'limit': 10 -} - -query = """ - SELECT {random_value} AS random_value, {null_value} AS null_value, u.user_id, sum(gross_amt) AS gross_amt - FROM {table_name_1!i} u - JOIN {table_name_2!q} p ON (u.user_id=p.user_id) - WHERE u.user_rating >= {user_rating!d} - AND u.user_score > {user_score!f} - AND u.is_female IS {is_female!r} - AND u.status IN ({user_statuses}) - AND u.user_rating NOT IN ({exclude_user_score!d}) - GROUP BY 1,2,3 - ORDER BY 4 DESC - LIMIT {limit!d} -""" - -stmt = C.execute(query, params) -print(stmt.query) -``` - -Result: -```sql -SELECT 'abc' AS random_value, NULL AS null_value, u.user_id, sum(gross_amt) AS gross_amt -FROM users u - JOIN "PYEXASOL_TEST"."PAYMENTS" p ON (u.user_id=p.user_id) -WHERE u.user_rating >= 0.5 - AND u.user_score > 10.0 - AND u.is_female IS TRUE - AND u.status IN ('ACTIVE', 'PASSIVE', 'SUSPENDED') - AND u.user_rating NOT IN (10, 20) -GROUP BY 1,2,3 -ORDER BY 4 DESC -LIMIT 10 -``` - -### IntelliJ IDE User Parameters - -It is possible to teach the IDE to recognize PyEXASOL placeholders in SQL strings. - -1. Settings -> Tools -> Database -> User Parameters -2. Add a new pattern: `\{\w+(\!\w)?\}` diff --git a/docs/img/parallel_export.png b/docs/img/parallel_export.png deleted file mode 100644 index 719fda4f493ea0dfa034287a3edb0454a665f053..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 100929 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

LnwhvWPnR|#QZZ1 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 g^?!cIe@i3gyb>AtM*yt-VK%;RVO7wxAvmjD0& diff --git a/noxfile.py b/noxfile.py index ca0bea2..45503db 100644 --- a/noxfile.py +++ b/noxfile.py @@ -145,4 +145,5 @@ def import_data(session: Session) -> None: build_docs, clean_docs, open_docs, + build_multiversion )