Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dbapi2 compatibility driver #145

Merged
merged 10 commits into from
Jul 4, 2024
11 changes: 6 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

## [Unreleased]

- Switch packaging and project workflows to poetry
- Drop support for python 3.7
- Drop support for Exasol 6.x
- Drop support for Exasol 7.0.x
- Relock dependencies (Internal)
- Added dbapi2 compliant driver interface `exasol.driver.websocket` ontop of pyexasol
- Switched packaging and project workflow to poetry
- Droped support for python 3.7
- Droped support for Exasol 6.x
- Droped support for Exasol 7.0.x
- Relocked dependencies (Internal)

## [0.25.2] - 2023-03-14

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ PyEXASOL provides API to read & write multiple data streams in parallel using se
## System requirements

- Exasol >= 7.1
- Python >= 3.8
- Python >= 3.9


## Getting started
Expand Down
53 changes: 22 additions & 31 deletions docs/DBAPI_COMPAT.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,28 @@
## 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.
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..

If you absolutely need DB-API 2.0 compatibility, you may use [TurbODBC](https://github.com/blue-yonder/turbodbc) instead.
### 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
Nicoretti marked this conversation as resolved.
Show resolved Hide resolved
[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

Expand Down Expand Up @@ -66,32 +86,3 @@ Replace with:
C.export_to_pandas('SELECT * FROM table')
```
etc.

## DB-API 2.0 wrapper

In order to make it easier to start using PyEXASOL, simple DB-API 2.0 wrapper is provided. It works for `SELECT` statements without placeholders. Please see the example:

```python
# Import "wrapped" version of PyEXASOL module
import pyexasol.db2 as E

C = E.connect(dsn=config.dsn, user=config.user, password=config.password, schema=config.schema)

# Cursor
cur = C.cursor()
cur.execute("SELECT * FROM users ORDER BY user_id LIMIT 5")

# Standard .description and .rowcount attributes
print(cur.description)
print(cur.rowcount)

# Standard fetching
while True:
row = cur.fetchone()

if row is None:
break

print(row)

```
Empty file added exasol/driver/__init__.py
Empty file.
Empty file.
165 changes: 165 additions & 0 deletions exasol/driver/websocket/_connection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
"""
This module provides `PEP-249`_ DBAPI compliant connection implementation.
(see also `PEP-249-connection`_)

.. _PEP-249-connection: https://peps.python.org/pep-0249/#connection-objects
"""

import ssl
from functools import wraps

import pyexasol

from exasol.driver.websocket._cursor import Cursor as DefaultCursor
from exasol.driver.websocket._errors import Error


def _requires_connection(method):
"""
Decorator requires the object to have a working connection.

Raises:
Error if the connection object has no active connection.
"""

@wraps(method)
def wrapper(self, *args, **kwargs):
if not self._connection:
raise Error("No active connection available")
return method(self, *args, **kwargs)

return wrapper


class Connection:
"""
Implementation of a websocket-based connection.

For more details see :class: `Connection` protocol definition.
"""

def __init__(
self,
dsn: str = None,
username: str = None,
password: str = None,
schema: str = "",
autocommit: bool = True,
tls: bool = True,
certificate_validation: bool = True,
client_name: str = "EXASOL:DBAPI2:WS",
client_version: str = "unknown",
):
"""
Create a Connection object.

Args:

dsn: Connection string, same format as for standard JDBC / ODBC drivers.
username: which will be used for the authentication.
password: which will be used for the authentication.
schema: to open after connecting.
autocommit: enable autocommit.
tls: enable tls.
certificate_validation: disable certificate validation.
client_name: which is communicated to the DB server.
"""

# for more details see pyexasol.connection.ExaConnection
self._options = {
"dsn": dsn,
"user": username,
"password": password,
"schema": schema,
"autocommit": autocommit,
"snapshot_transactions": None,
"connection_timeout": 10,
"socket_timeout": 30,
"query_timeout": 0,
"compression": False,
"encryption": tls,
"fetch_dict": False,
"fetch_mapper": None,
"fetch_size_bytes": 5 * 1024 * 1024,
"lower_ident": False,
"quote_ident": False,
"json_lib": "json",
"verbose_error": True,
"debug": False,
"debug_logdir": None,
"udf_output_bind_address": None,
"udf_output_connect_address": None,
"udf_output_dir": None,
"http_proxy": None,
"client_name": client_name,
"client_version": client_version,
"protocol_version": 3,
"websocket_sslopt": (
{"cert_reqs": ssl.CERT_REQUIRED} if certificate_validation else None
),
"access_token": None,
"refresh_token": None,
}
self._connection = None

def connect(self):
"""See also :py:meth: `Connection.connect`"""
try:
self._connection = pyexasol.connect(**self._options)
except pyexasol.exceptions.ExaConnectionError as ex:
raise Error(f"Connection failed, {ex}") from ex
except Exception as ex:
raise Error() from ex
return self

@property
def connection(self):
"""Underlying connection used by this Connection"""
return self._connection

def close(self):
"""See also :py:meth: `Connection.close`"""
connection_to_close = self._connection
self._connection = None
if connection_to_close is None or connection_to_close.is_closed:
return
try:
connection_to_close.close()
except Exception as ex:
raise Error() from ex

@_requires_connection
def commit(self):
"""See also :py:meth: `Connection.commit`"""
try:
self._connection.commit()
except Exception as ex:
raise Error() from ex

@_requires_connection
def rollback(self):
"""See also :py:meth: `Connection.rollback`"""
try:
self._connection.rollback()
except Exception as ex:
raise Error() from ex

@_requires_connection
def cursor(self):
"""See also :py:meth: `Connection.cursor`"""
return DefaultCursor(self)

def __del__(self):
if self._connection is None:
return

# Currently, the only way to handle this gracefully is to invoke the`__del__`
# method of the underlying connection rather than calling an explicit `close`.
#
# For more details, see also:
# * https://github.com/exasol/sqlalchemy-exasol/issues/390
# * https://github.com/exasol/pyexasol/issues/108
#
# If the above tickets are resolved, it should be safe to switch back to using
# `close` instead of `__del__`.
self._connection.__del__()
Loading
Loading