Skip to content

Commit

Permalink
Refinements to GraphQLDriver
Browse files Browse the repository at this point in the history
  • Loading branch information
kenstott committed Nov 7, 2024
1 parent 1ae5062 commit d897c49
Show file tree
Hide file tree
Showing 5 changed files with 278 additions and 24 deletions.
176 changes: 176 additions & 0 deletions calcite-rs-jni/jdbc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# GraphQL JDBC Connector

A JDBC driver that enables SQL access to GraphQL endpoints using Apache Calcite. This connector allows you to query GraphQL APIs using standard SQL syntax, making it easy to integrate GraphQL data sources with existing SQL-based tools and applications.

## Features

- Execute SQL queries against GraphQL endpoints
- Support for authentication via headers (Bearer tokens, API keys)
- Role-based access control support
- Query result caching (in-memory or Redis)
- Connection URL configuration options
- Integration with standard JDBC tooling
- Built on Apache Calcite for robust SQL support

## Installation

Add the following dependency to your project:

```xml
<dependency>
<groupId>com.hasura</groupId>
<artifactId>graphql-jdbc-driver</artifactId>
<version>1.0.0</version>
</dependency>
```

## Usage

### JDBC URL Format

The basic JDBC URL format is:

```
jdbc:graphql:<graphql-endpoint>[;<option>=<value>...]
```

Example URLs:
```
jdbc:graphql:http://localhost:8080/v1/graphql
jdbc:graphql:https://api.example.com/graphql;role=admin;auth=Bearer123
```

### Connection Options

Options can be specified in the JDBC URL after the endpoint, separated by semicolons:

#### Special Options
- `user` - GraphQL user identifier
- `role` - User role for role-based access control
- `auth` - Authentication token/header

#### Cache Options
The connector supports query result caching to improve performance. Configure caching using these operand options:

```
jdbc:graphql:http://localhost:8080/v1/graphql;operand.cache.type=memory;operand.cache.ttl=300
```

Available cache options:
- `operand.cache.type` - Cache implementation to use:
- `memory` - In-memory cache using Guava Cache
- `redis` - Redis-based distributed cache
- If not specified, caching is disabled
- `operand.cache.ttl` - Cache time-to-live in seconds (defaults to 300)
- `operand.cache.url` - Redis connection URL (required if cache.type is "redis")

Example configurations:
```
# In-memory cache with 5 minute TTL
jdbc:graphql:http://localhost:8080/v1/graphql;operand.cache.type=memory;operand.cache.ttl=300
# Redis cache with 10 minute TTL
jdbc:graphql:http://localhost:8080/v1/graphql;operand.cache.type=redis;operand.cache.ttl=600;operand.cache.url=redis://localhost:6379
# Disable caching
jdbc:graphql:http://localhost:8080/v1/graphql
```

The cache is implemented as a singleton per JVM, meaning:
- For in-memory caching, the cache is shared across all connections in the same JVM
- For Redis caching, the cache can be shared across multiple JVMs using the same Redis instance

#### Operand Options
Prefix with `operand.` to pass custom options to the GraphQL adapter:
```
jdbc:graphql:http://localhost:8080/v1/graphql;operand.timeout=30;operand.maxRows=1000
```

#### Calcite Options
Prefix with `calcite.` to configure underlying Calcite behavior:
```
jdbc:graphql:http://localhost:8080/v1/graphql;calcite.caseSensitive=false
```

### Java Example

```java
// Basic connection
String url = "jdbc:graphql:http://localhost:8080/v1/graphql";
Connection conn = DriverManager.getConnection(url);

// Connection with options
String url = "jdbc:graphql:http://localhost:8080/v1/graphql;role=admin;auth=Bearer123";
Connection conn = DriverManager.getConnection(url);

// Connection with caching
String url = "jdbc:graphql:http://localhost:8080/v1/graphql;operand.cache.type=memory;operand.cache.ttl=300";
Connection conn = DriverManager.getConnection(url);

// Execute SQL query
try (Statement stmt = conn.createStatement()) {
ResultSet rs = stmt.executeQuery("SELECT id, name, email FROM users WHERE age > 21");
while (rs.next()) {
System.out.println(rs.getString("name"));
}
}
```

### Properties File Example

```properties
jdbc.driver=com.hasura.GraphQLDriver
jdbc.url=jdbc:graphql:http://localhost:8080/v1/graphql
jdbc.user=admin
jdbc.auth=Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```

## Configuration Properties

| Property | Description | Default |
|----------|-------------|---------|
| `user` | GraphQL user identifier | None |
| `role` | User role for RBAC | None |
| `auth` | Authentication token/header | None |
| `operand.timeout` | Query timeout in seconds | 30 |
| `operand.maxRows` | Maximum rows to return | 1000 |
| `operand.cache.type` | Cache implementation (`memory` or `redis`) | None |
| `operand.cache.ttl` | Cache time-to-live in seconds | 300 |
| `operand.cache.url` | Redis connection URL | redis://localhost:6379 |
| `calcite.caseSensitive` | Case sensitivity for identifiers | true |
| `calcite.unquotedCasing` | Unquoted identifier casing | UNCHANGED |
| `calcite.quotedCasing` | Quoted identifier casing | UNCHANGED |

## SQL Support

The connector supports standard SQL2003 operations including:
- SELECT queries with filtering and joins
- Aggregations (COUNT, SUM, etc.)
- ORDER BY and GROUP BY clauses
- LIMIT and OFFSET

The actual SQL capabilities depend on the underlying GraphQL schema and endpoint capabilities.

You can read more about the advanced capabilities, [here](../calcite/graphql/docs/features.md).

## Building from Source

```bash
git clone https://github.com/hasura/graphql-jdbc-driver.git
cd graphql-jdbc-driver
mvn clean package
```

## Contributing

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

## License

This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.

## Support

For issues and feature requests, please file an issue on the GitHub repository.

For commercial support, please contact [email protected].
19 changes: 15 additions & 4 deletions calcite-rs-jni/jdbc/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j.version}</version>
</dependency>

<!-- Apache Commons -->
<dependency>
Expand Down Expand Up @@ -251,6 +256,12 @@
<directory>src/main/resources</directory>
<filtering>false</filtering>
</resource>
<!-- Add specific resource for service file -->
<resource>
<directory>src/main/service</directory>
<targetPath>META-INF/services</targetPath>
<filtering>false</filtering>
</resource>
</resources>
<plugins>
<!-- Enforcer Plugin -->
Expand Down Expand Up @@ -338,14 +349,14 @@
</executions>
</plugin>

<!-- Assembly Plugin for Fat JAR -->
<!-- Assembly Plugin -->
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.7.1</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<descriptors>
<descriptor>src/assembly/assembly.xml</descriptor>
</descriptors>
<archive>
<manifest>
<addClasspath>true</addClasspath>
Expand Down
30 changes: 30 additions & 0 deletions calcite-rs-jni/jdbc/src/assembly/assembly.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.1.0 http://maven.apache.org/xsd/assembly-2.1.0.xsd">
<id>jar-with-dependencies</id>
<formats>
<format>jar</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>

<fileSets>
<fileSet>
<directory>${project.build.outputDirectory}</directory>
<outputDirectory>/</outputDirectory>
</fileSet>
</fileSets>

<dependencySets>
<dependencySet>
<outputDirectory>/</outputDirectory>
<useProjectArtifact>false</useProjectArtifact>
<unpack>true</unpack>
<scope>runtime</scope>
<unpackOptions>
<excludes>
<exclude>META-INF/services/java.sql.Driver</exclude>
</excludes>
</unpackOptions>
</dependencySet>
</dependencySets>
</assembly>
76 changes: 57 additions & 19 deletions calcite-rs-jni/jdbc/src/main/java/com/hasura/GraphQLDriver.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,23 @@
import org.apache.calcite.avatica.AvaticaConnection;
import org.apache.calcite.avatica.DriverVersion;
import org.apache.calcite.avatica.Meta;
import org.apache.calcite.avatica.UnregisteredDriver;
import org.apache.calcite.avatica.MetaImpl;
import org.apache.calcite.jdbc.Driver;
import org.apache.calcite.jdbc.CalciteConnection;
import org.apache.calcite.schema.SchemaPlus;
import org.apache.calcite.sql.SqlDialect;

import java.sql.*;
import java.util.Properties;
import java.util.Map;
import java.util.HashMap;
import java.util.logging.Logger;

public class GraphQLDriver extends UnregisteredDriver implements Driver {
public class GraphQLDriver extends Driver {
private static final String PREFIX = "jdbc:graphql:";

static {
try {
Class.forName("org.apache.calcite.avatica.remote.Driver");
Class.forName("org.apache.calcite.jdbc.Driver");
DriverManager.registerDriver(new GraphQLDriver());
} catch (Exception e) {
throw new RuntimeException("Failed to register GraphQL JDBC driver", e);
Expand All @@ -47,33 +47,61 @@ protected DriverVersion createDriverVersion() {
);
}

private void parseUrlOptions(String url, Map<String, Object> operand, Properties calciteProps) {
String[] parts = url.split(";");
for (int i = 1; i < parts.length; i++) {
String option = parts[i].trim();
if (option.isEmpty()) continue;

String[] keyValue = option.split("=", 2);
if (keyValue.length != 2) continue;

String key = keyValue[0].trim();
String value = keyValue[1].trim();

if (key.startsWith("operand.")) {
String operandKey = key.substring("operand.".length());
if (operandKey.contains(".")) {
String[] nestedKeys = operandKey.split("\\.", 2);
Map<String, Object> nestedMap = (Map<String, Object>) operand.computeIfAbsent(nestedKeys[0], k -> new HashMap<String, Object>());
nestedMap.put(nestedKeys[1], value);
} else {
operand.put(operandKey, value);
}
} else if (key.startsWith("calcite.")) {
String calciteKey = key.substring("calcite.".length());
calciteProps.setProperty(calciteKey, value);
} else if (key.equals("user") || key.equals("role") || key.equals("auth")) {
operand.put(key, value);
}
}
}

@Override
public Connection connect(String url, Properties info) throws SQLException {
if (!acceptsURL(url)) {
return null;
}

String connectionUrl = url.substring(PREFIX.length());
String urlWithoutPrefix = url.substring(PREFIX.length());
String[] parts = urlWithoutPrefix.split(";");
String baseUrl = parts[0];

Map<String, Object> operand = new HashMap<>();
operand.put("endpoint", connectionUrl);
operand.put("endpoint", baseUrl);

if (info.containsKey("user")) {
operand.put("user", info.getProperty("user"));
}
if (info.containsKey("role")) {
operand.put("role", info.getProperty("role"));
}
if (info.containsKey("auth")) {
operand.put("auth", info.getProperty("auth"));
}
if (info.containsKey("user")) operand.put("user", info.getProperty("user"));
if (info.containsKey("role")) operand.put("role", info.getProperty("role"));
if (info.containsKey("auth")) operand.put("auth", info.getProperty("auth"));

Properties calciteProps = new Properties();
calciteProps.setProperty("fun", "standard");
calciteProps.setProperty("caseSensitive", "true");
calciteProps.setProperty("unquotedCasing", "UNCHANGED");
calciteProps.setProperty("quotedCasing", "UNCHANGED");

parseUrlOptions(urlWithoutPrefix, operand, calciteProps);

Connection connection = DriverManager.getConnection("jdbc:calcite:", calciteProps);
CalciteConnection calciteConnection = connection.unwrap(CalciteConnection.class);

Expand All @@ -84,16 +112,26 @@ public Connection connect(String url, Properties info) throws SQLException {

calciteConnection.setSchema("GRAPHQL");

// Wrap the connection with our enhanced metadata support
return connection;
}

@Override
public Logger getParentLogger() {
return null;
public boolean acceptsURL(String url) {
return url != null && url.startsWith(PREFIX);
}

@Override
public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) {
return new DriverPropertyInfo[0];
}

@Override
public Meta createMeta(AvaticaConnection connection) {
return null;
public boolean jdbcCompliant() {
return true;
}
@Override
public Logger getParentLogger() {
return Logger.getLogger(getClass().getPackage().getName());
}
}
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
com.hasura.GraphQLDriver

0 comments on commit d897c49

Please sign in to comment.