An open API service indexing awesome lists of open source software.

https://github.com/rqlite/rqlite-jdbc

Type 4 JDBC driver for rqlite
https://github.com/rqlite/rqlite-jdbc

java jdbc jdbc-driver

Last synced: 3 months ago
JSON representation

Type 4 JDBC driver for rqlite

Awesome Lists containing this project

README

          

# rqlite JDBC Driver

This is a minimal (~130KB, zero dependencies), Type 4 JDBC driver for [rqlite](https://github.com/rqlite/rqlite),
a lightweight, distributed relational database built on top of SQLite.

This driver enables Java applications to interact with `rqlite` over HTTP, supporting standard
JDBC operations like queries, updates, and batch processing in a clustered environment.

## Features

- **JDBC Compliance**: Supports core JDBC APIs, including `Connection`, `Statement`, `PreparedStatement`, and `ResultSet`.
- **Atomic Transactions**: Executes multiple statements atomically using rqlite’s `transaction=true` mode via batch operations.
- **Clustered Environment Support**: Configurable options for read consistency, write queuing, and timeouts to handle rqlite’s distributed nature.
- **Schema and Metadata Access**: Query table metadata, primary keys, foreign keys, and indexes (see [L4DriverTest](./src/test/java/io/rqlite/L4DriverTest.java)).

## Getting Started

### Prerequisites

- Java 11 or higher
- `rqlite` server running (e.g., `http://localhost:4001`)

Install from [Maven Central](https://mvnrepository.com/artifact/io.rqlite/rqlite-jdbc)

io.rqlite:rqlite-jdbc:[version]

The driver version corresponds to the last known `rqlite` [release](https://github.com/rqlite/rqlite/releases) the driver was tested against, followed by a build version of the driver itself.

## Basic Usage

Connect to an `rqlite` instance and execute queries using standard JDBC APIs.

```
import java.sql.*;

var url = "jdbc:rqlite:http://localhost:4001";

try (Connection conn = DriverManager.getConnection(url)) {
var stmt = conn.createStatement();
stmt.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)");

var ps = conn.prepareStatement("INSERT INTO users (name, age) VALUES (?, ?)");
ps.setString(1, "Alice");
ps.setInt(2, 30);
ps.executeUpdate();

var rs = stmt.executeQuery("SELECT * FROM users");
while (rs.next()) {
System.out.println("ID: " + rs.getInt("id") + ", Name: " + rs.getString("name") + ", Age: " + rs.getInt("age"));
}
}
```

## Batch Processing for Transactions

`rqlite` executes statements atomically with `transaction=true`. Use batch operations for multi-statement transactions.

Using `Statement`:

```
Statement stmt = conn.createStatement();
stmt.addBatch("INSERT INTO users (name, age) VALUES ('Fiona', 25)");
stmt.addBatch("INSERT INTO users (name, age) VALUES ('Sinead', 28)");
int[] updateCounts = stmt.executeBatch(); // Executes atomically
```

Using `PreparedStatement`:

```
PreparedStatement ps = conn.prepareStatement("INSERT INTO users (name, age) VALUES (?, ?)");
ps.setString(1, "Fiona");
ps.setInt(2, 25);
ps.addBatch();
ps.setString(1, "Sinead");
ps.setInt(2, 28);
ps.addBatch();
int[] updateCounts = ps.executeBatch(); // Executes atomically
```

See [L4PsTest](./src/test/java/io/rqlite/L4PsTest.java) for advanced examples with various data types, streams, and LOBs.

## Configuration Options

Customize the driver’s behavior via JDBC URL parameters, see [L4Options](./src/main/java/io/rqlite/rqlite/L4Options.java). Below are the available options, their defaults, and their purposes.

These options come from `rqlite`'s [Developer Guide](https://rqlite.io/docs/api)

| Property Key | Type | Default Value | Description |
|-----------------------------|-----------|--------------------------|-----------------------------------------------------------------------------|
| `baseUrl` | `String` | `null` | The base URL of the RQLite server (e.g., `http://localhost:4001`). |
| `user` | `String` | `null` | Username for RQLite server authentication. |
| `password` | `String` | `null` | Password for RQLite server authentication. |
| `cacert` | `String` | `null` | Path to the CA certificate for SSL/TLS connections. |
| `insecure` | `boolean` | `false` | If `true`, disables SSL/TLS verification (not recommended for production). |
| `timeoutSec` | `long` | `5` | Timeout for HTTP requests in seconds. |
| `queue` | `boolean` | `false` | If `true`, enables queuing of requests on the RQLite server. |
| `wait` | `boolean` | `true` | If `true`, waits for the request to be processed by the RQLite leader. |
| `level` | `L4Level` | `L4Level.linearizable` | Consistency level for queries (`none`, `weak`, `strong`, `linearizable`). |
| `linearizableTimeoutSec` | `long` | `5` | Timeout for linearizable consistency queries in seconds. |
| `freshnessSec` | `long` | `5` | Maximum age of data for freshness-based queries in seconds. |
| `freshnessStrict` | `boolean` | `false` | If `true`, enforces strict freshness for queries. |

Example JDBC URL:

```java
String url = "jdbc:rqlite:http://localhost:4001?timeoutSec=5&level=strong&freshnessSec=1";
```

## Caveats

### Memory Usage

Result sets are held in memory (mapped from rqlite’s JSON responses to JDBC ResultSet). Write queries that return small datasets to avoid memory issues.

### Catalog Support

Only the `main` SQLite database is reported as a catalog to JDBC.

### Transaction Limitations

The driver offers deferred transaction support on `Connection` instances due to `rqlite`'s [Transaction support](https://rqlite.io/docs/api/api/#transactions) conventions, which deviate from the JDBC standard.

To execute multiple SQL statements as a transaction, you have two options.

#### Batch statements

Populate a JDBC batch using [`Statement`](https://docs.oracle.com/en/java/javase/11/docs/api/java.sql/java/sql/Statement.html#addBatch(java.lang.String)) or [`PreparedStatement`](https://docs.oracle.com/en/java/javase/11/docs/api/java.sql/java/sql/PreparedStatement.html#addBatch()), which will get sent with `transaction=true` to the underlying `rqlite` HTTP request.

#### Synthetic/deferred transactions

Call `setAutoCommit(false)` on a [`Connection`](https://docs.oracle.com/en/java/javase/11/docs/api/java.sql/java/sql/Connection.html#setAutoCommit(boolean)), and run `insert`, `update` or `delete` statements.

The execution of these statements will get deferred until you call `commit()`.

This will send all statements to the database as a single batch, appending `transaction=true` to the underlying `rqlite` HTTP request.

This implies that you won't be able to inspect `ResultSet`s, metadata or row counts after executing each statement. A dummy resultset is provided only for compatibility with JDBC semantics.

The only guarantee is that if `commit()` succeeds, then all deferred statements were accepted by the database.

Lastly, make sure that all statements get executed through the same connection where the transaction was initiated.

### Isolation Level

Only `TRANSACTION_SERIALIZABLE` is supported, with `linearizable` read consistency by default. Setting `level=weak` or `level=none` may introduce read inconsistencies.

### Type Mapping

User-defined SQL types (UDTs) are not supported. `getTypeMap` and `setTypeMap` are implemented for compliance but have no effect.

### Date/Time Handling

The driver normalizes all `java.sql.Date`, `Time`, and `Timestamp` values to UTC before storage in rqlite, ensuring consistent round-trip behavior regardless of the JVM's default timezone.

This is necessary because rqlite normalizes date/time outputs for typed columns (`DATE`, `DATETIME`, `TIMESTAMP`) to ISO 8601 format with a `'Z'` suffix (indicating UTC), which could otherwise cause shifts in the retrieved instant if local timezones are involved.

#### Setters (`setDate`, `setTime`, `setTimestamp`)

The input value's instant (milliseconds since UTC epoch) is converted to UTC components and stored as a string (e.g., `'2025-08-23 13:00:00'` for timestamps, without `'Z'`). The optional `Calendar` parameter is ignored, as forcing UTC normalization preserves the original instant across environments.

If timezone-specific adjustments are needed, perform them in your application before creating the `Date`/`Time`/`Timestamp` objects.

#### Getters (`getDate`, `getTime`, `getTimestamp`)

Values are parsed assuming UTC (via `Instant.parse` for ISO strings with `'Z'`). The returned objects hold accurate UTC milliseconds internally.

However, due to legacy `java.sql` types:

- `toString()` on `Date`/`Time`/`Timestamp` formats components in the JVM's default timezone, which may show shifted values (e.g., a UTC midnight might display as the previous day in EST). This is a display artifact—the underlying instant is unchanged.
- To ensure consistent display or interpretation, provide a `Calendar` set to UTC (e.g., `Calendar.getInstance(TimeZone.getTimeZone("UTC"))`) in getters.

#### Recommendations:

- Store all dates/times in UTC to avoid complexity.
- Test in multiple JVM timezones (e.g., via `-Duser.timezone=UTC`) to verify behavior.
- If preserving original timezones is critical, store offsets or timezone names in separate columns, as rqlite/SQLite does not natively support timezone-aware types.

This approach deviates slightly from standard JDBC for timezone-agnostic databases but prioritizes reliability with rqlite's output quirks.

For examples, see [L4PsTest](./src/test/java/io/rqlite/L4PsTest.java).

## Contributing

Contributions are welcome! Please submit issues or pull requests to this GitHub repository.

Requires Gradle 8.1 or later.

Create a file with the following content at `~/.gsOrgConfig.json`:

```
{
"orgConfigUrl": "https://raw.githubusercontent.com/rqlite/rqlite-jdbc/refs/heads/org-config/org-config.json"
}
```

Then run:

```
gradle clean build
```