This is a minimal (~130KB, zero dependencies), Type 4 JDBC driver for 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.
- JDBC Compliance: Supports core JDBC APIs, including
Connection,Statement,PreparedStatement, andResultSet. - Atomic Transactions: Executes multiple statements atomically using rqlite’s
transaction=truemode 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).
- Java 11 or higher
rqliteserver running (e.g.,http://localhost:4001)
Install from Maven Central
io.rqlite:rqlite-jdbc:[version]
The driver version corresponds to the last known rqlite release the driver was tested against, followed by a build version of the driver itself.
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"));
}
}
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 for advanced examples with various data types, streams, and LOBs.
Customize the driver’s behavior via JDBC URL parameters, see L4Options. Below are the available options, their defaults, and their purposes.
These options come from rqlite's Developer Guide
| 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:
String url = "jdbc:rqlite:http://localhost:4001?timeoutSec=5&level=strong&freshnessSec=1";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.
Only the main SQLite database is reported as a catalog to JDBC.
The driver offers deferred transaction support on Connection instances due to rqlite's Transaction support conventions, which deviate from the JDBC standard.
To execute multiple SQL statements as a transaction, you have two options.
Populate a JDBC batch using Statement or PreparedStatement, which will get sent with transaction=true to the underlying rqlite HTTP request.
Certain ORM frameworks (e.g., JetBrains Exposed) may require specific driver metadata for compatibility.
If you encounter issues with the reported database product name, use L4DbMeta.setDriverName("SQLite JDBC") to override the default value before connecting.
This can help align the driver with framework expectations for SQLite-like dialects.
See also: L4ExposedTest
Call setAutoCommit(false) on a Connection, 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 ResultSets, 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.
Only TRANSACTION_SERIALIZABLE is supported, with linearizable read consistency by default. Setting level=weak or level=none may introduce read inconsistencies.
User-defined SQL types (UDTs) are not supported. getTypeMap and setTypeMap are implemented for compliance but have no effect.
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.
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.
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()onDate/Time/Timestampformats 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
Calendarset to UTC (e.g.,Calendar.getInstance(TimeZone.getTimeZone("UTC"))) in getters.
- 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.
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