Skip to content

Commit

Permalink
Added a python DB API driver for GraphQLDriver
Browse files Browse the repository at this point in the history
  • Loading branch information
kenstott committed Nov 7, 2024
1 parent c7a00b6 commit 1ae5062
Show file tree
Hide file tree
Showing 9 changed files with 608 additions and 0 deletions.
43 changes: 43 additions & 0 deletions calcite-rs-jni/py_graphql_sql/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# Virtual Environment
.env
.venv
env/
venv/
ENV/

# IDE
.idea/
.vscode/
*.swp
*.swo

# Tests
.coverage
htmlcov/
.pytest_cache/
.mypy_cache/

# Logs
*.log
157 changes: 157 additions & 0 deletions calcite-rs-jni/py_graphql_sql/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# Python DB-API for Hasura DDN

This is a Python DB-API 2.0 compliant implementation for connecting to Hasura DDN endpoints using SQL through the Hasura GraphQL JDBC driver. It allows you to query Hasura DDN endpoints using SQL:2003 syntax through a JDBC bridge.

## Installation

```bash
# Using poetry (recommended)
poetry add python-db-api

# Or using pip
pip install python-db-api
```

## Prerequisites

1. Python 3.9 or higher
2. Java JDK 11 or higher installed and accessible in your system path
3. `graphql-jdbc-driver-1.0.0-jar-with-dependencies.jar` - this single JAR file contains all required dependencies

## Basic Usage

Here's a simple example of how to use the DB-API:

```python
from python_db_api import connect
import os

# Connection parameters
host = "http://localhost:3000/graphql" # Your Hasura DDN endpoint
jdbc_args = {"role": "admin"} # Connection properties

# Path to directory containing the all-in-one driver JAR
driver_paths = ["/path/to/jdbc/target"] # Directory containing graphql-jdbc-driver-1.0.0-jar-with-dependencies.jar

# Create connection using context manager
with connect(host, jdbc_args, driver_paths) as conn:
with conn.cursor() as cur:
# Execute SQL:2003 query
cur.execute("SELECT * FROM Albums")

# Fetch results
for row in cur.fetchall():
print(f"Result: {row}")
```

## Connection Parameters

### Required Parameters

- `host`: The Hasura DDN endpoint URL (e.g., "http://localhost:3000/graphql")
- `driver_paths`: List containing the directory path where `graphql-jdbc-driver-1.0.0-jar-with-dependencies.jar` is located

### Optional Parameters

- `jdbc_args`: Dictionary of connection properties
- Supported properties: "role", "user", "auth"
- Example: `{"role": "admin", "auth": "bearer token"}`

## Connection Properties

You can pass various connection properties through the `jdbc_args` parameter:

```python
jdbc_args = {
"role": "admin", # Hasura role
"user": "username", # Optional username
"auth": "token" # Optional auth token
}
```

## Directory Structure

The driver requires a single JAR file. Example structure:

```
/path/to/jdbc/
└── target/
└── graphql-jdbc-driver-1.0.0-jar-with-dependencies.jar
```

## Error Handling

The implementation provides clear error messages for common issues:

```python
try:
with connect(host, jdbc_args, driver_paths) as conn:
# ... your code ...
except DatabaseError as e:
print(f"Database error occurred: {e}")
```

Common errors:
- Missing driver JAR file
- Invalid driver path
- Connection failures
- Invalid SQL:2003 queries

## Context Manager Support

The implementation supports both context manager and traditional connection patterns:

```python
# Using context manager (recommended)
with connect(host, jdbc_args, driver_paths) as conn:
# ... your code ...

# Traditional approach
conn = connect(host, jdbc_args, driver_paths)
try:
# ... your code ...
finally:
conn.close()
```

## Type Hints

The implementation includes type hints for better IDE support and code completion:

```python
from python_db_api import connect
from typing import List

def get_connection(
host: str,
properties: dict[str, str],
paths: List[str]
) -> None:
with connect(host, properties, paths) as conn:
# Your code here
pass
```

## Thread Safety

The connection is not thread-safe. Each thread should create its own connection instance.

## Dependencies

- `jaydebeapi`: Java Database Connectivity (JDBC) bridge
- `jpype1`: Java to Python integration
- Java JDK 11+
- `graphql-jdbc-driver-1.0.0-jar-with-dependencies.jar`

## Limitations

- One JVM per Python process
- Cannot modify classpath after JVM starts

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

## License

This project is licensed under the MIT License.
31 changes: 31 additions & 0 deletions calcite-rs-jni/py_graphql_sql/examples/basic_usage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Example usage of the DB-API implementation."""
from py_graphql_sql import connect
import os

def main() -> None:
"""Basic example of connecting to a database and executing queries."""
# Connection parameters
host = "http://localhost:3000/graphql"
jdbc_args = {"role": "admin"}

# Get paths to JAR directories
current_dir = os.path.dirname(os.path.abspath(__file__))
driver_paths = [
os.path.abspath(os.path.join(current_dir, "../../jdbc/target")) # Add additional paths as needed
]

# Create connection using context manager
with connect(host, jdbc_args, driver_paths) as conn:
with conn.cursor() as cur:
# Execute a query
cur.execute("SELECT * FROM Albums", [])

# Fetch all results
rows = cur.fetchall()

# Display all rows
for row in rows:
print(f"Result: {row}")

if __name__ == "__main__":
main()
33 changes: 33 additions & 0 deletions calcite-rs-jni/py_graphql_sql/py_graphql_sql/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""DB-API 2.0 compliant JDBC connector."""

from .connection import Connection, connect
from .cursor import Cursor
from .exceptions import (
Error, Warning, InterfaceError, DatabaseError,
DataError, OperationalError, IntegrityError,
InternalError, ProgrammingError, NotSupportedError
)

# DB-API 2.0 required globals
apilevel = '2.0'
threadsafety = 1 # Threads may share the module but not connections
paramstyle = 'qmark' # Question mark style, e.g. ...WHERE name=?

__all__ = [
'Connection',
'Cursor',
'connect',
'apilevel',
'threadsafety',
'paramstyle',
'Error',
'Warning',
'InterfaceError',
'DatabaseError',
'DataError',
'OperationalError',
'IntegrityError',
'InternalError',
'ProgrammingError',
'NotSupportedError',
]
115 changes: 115 additions & 0 deletions calcite-rs-jni/py_graphql_sql/py_graphql_sql/connection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""DB-API 2.0 Connection implementation."""
from __future__ import annotations
from contextlib import AbstractContextManager
from typing import Optional, Any, List
import jaydebeapi
import jpype
import os
import glob

from .exceptions import DatabaseError
from .db_types import JDBCArgs, JDBCPath

JDBC_DRIVER = "com.hasura.GraphQLDriver"
EXCLUDED_JAR = "graphql-jdbc-driver-1.0.0.jar"

class Connection(AbstractContextManager['Connection']):
"""DB-API 2.0 Connection class."""

def __init__(
self,
host: str,
jdbc_args: JDBCArgs = None,
driver_paths: List[str] = None
) -> None:
"""Initialize connection."""
try:
# Start JVM if it's not already started
if not jpype.isJVMStarted():
# Build classpath from all JARs in provided directories
classpath = []
if driver_paths:
for path in driver_paths:
if not os.path.exists(path):
raise DatabaseError(f"Driver path not found: {path}")

# Find all JAR files in the directory
jar_files = glob.glob(os.path.join(path, "*.jar"))

# Add all JARs except the excluded one
for jar in jar_files:
if os.path.basename(jar) != EXCLUDED_JAR:
classpath.append(jar)

if not classpath:
raise DatabaseError("No JAR files found in provided paths")

# Join all paths with OS-specific path separator
classpath_str = os.pathsep.join(classpath)

jpype.startJVM(
jpype.getDefaultJVMPath(),
f"-Djava.class.path={classpath_str}",
convertStrings=True
)

# Construct JDBC URL
jdbc_url = f"jdbc:graphql:{host}"

# Create Properties object
props = jpype.JClass('java.util.Properties')()

# Add any properties from jdbc_args
if jdbc_args:
if isinstance(jdbc_args, dict):
for key, value in jdbc_args.items():
props.setProperty(key, str(value))
elif isinstance(jdbc_args, list) and len(jdbc_args) > 0:
props.setProperty("role", jdbc_args[0])

# Connect using URL and properties
self._jdbc_connection = jaydebeapi.connect(
jclassname=JDBC_DRIVER,
url=jdbc_url,
driver_args=[props],
jars=None
)
self.closed: bool = False
except Exception as e:
raise DatabaseError(f"Failed to connect: {str(e)}") from e

def __enter__(self) -> 'Connection':
"""Enter context manager."""
return self

def __exit__(self, exc_type: Optional[type], exc_val: Optional[Exception],
exc_tb: Optional[Any]) -> None:
"""Exit context manager."""
self.close()

def close(self) -> None:
"""Close the connection."""
if not self.closed:
self._jdbc_connection.close()
self.closed = True

def cursor(self):
"""Create a new cursor."""
if self.closed:
raise DatabaseError("Connection is closed")
return self._jdbc_connection.cursor()

def connect(
host: str,
jdbc_args: JDBCArgs = None,
driver_paths: List[str] = None,
) -> Connection:
"""
Create a new database connection.
Args:
host: The GraphQL server host (e.g., 'http://localhost:3000/graphql')
jdbc_args: Optional connection arguments (dict or list)
driver_paths: List of paths to directories containing JAR files
"""
return Connection(host, jdbc_args, driver_paths)
Loading

0 comments on commit 1ae5062

Please sign in to comment.