A lightweight Java client that speaks the Hrana protocol over HTTP to LibSQL/Turso-compatible backends. You can use it in two ways:
- Low-level: call the Hrana HTTP pipeline directly via
HranaHttpClientandHranaHttpStream. - JDBC: use the minimal JDBC driver to work with
java.sql(Connection,Statement,PreparedStatement,ResultSet).
This project is still under development and will probably require some additional work before the JDBC interface will work completely.
Add the following maven dependency:
<dependency>
<groupId>com.bedatadriven</groupId>
<artifactId>hrana-client</artifactId>
<version>0.1</version>
</dependency>- JDBC
Driverforjdbc:libsql://...URLs - Low-level API:
HranaHttpClient/HranaHttpStreamfor direct Hrana v3 over HTTP (no JDBC) - HTTP/2 client using Java 11+
HttpClient - Basic SQL execution via
StatementandPreparedStatement - Transaction support with
setAutoCommit,commit,rollback - JWT-based authentication
- Tested against Hrana v3 protobuf pipeline
- Choose low-level (
HranaHttpClient/HranaHttpStream) when you want:- Minimal surface area and control over Hrana-specific features (pipeline, baton, stickiness)
- Smaller dependency footprint and straightforward use in serverless or non-blocking contexts
- Direct access to protobuf results (
StmtResult) and manual mapping
- Choose JDBC when you want:
- Compatibility with existing code that uses
java.sql.* - Familiar
Connection/Statement/PreparedStatement/ResultSetAPIs - Easier migration path for legacy applications
- Compatibility with existing code that uses
- Java 17 or newer
- A LibSQL/Turso-compatible endpoint that supports Hrana over HTTPS
- A JWT token with appropriate permissions
The driver registers automatically when the JAR is on the classpath, so you can connect
with DriverManager:
import java.sql.*;
import java.util.Properties;
public class Demo {
public static void main(String[] args) throws Exception {
// Option A: JWT in the URL
try (Connection conn = DriverManager.getConnection(
"jdbc:libsql://mydb.turso.io/my_namespace?jwt=YOUR_TOKEN")) {
runDemo(conn);
}
// Option B: JWT via Properties
Properties props = new Properties();
props.setProperty("jwt", System.getenv("HRANA_JWT"));
try (Connection conn = DriverManager.getConnection(
"jdbc:libsql://mydb.turso.io/my_namespace", props)) {
runDemo(conn);
}
// Option C: JWT via system property or env var HRANA_JWT
// -DHRANA_JWT=YOUR_TOKEN or export HRANA_JWT=YOUR_TOKEN
try (Connection conn = DriverManager.getConnection(
"jdbc:libsql://mydb.turso.io/my_namespace")) {
runDemo(conn);
}
}
private static void runDemo(Connection conn) throws Exception {
try (Statement st = conn.createStatement()) {
st.executeUpdate("CREATE TABLE IF NOT EXISTS demo (id INTEGER PRIMARY KEY, name TEXT)");
st.executeUpdate("DELETE FROM demo");
st.executeUpdate("INSERT INTO demo (id, name) VALUES (1, 'Alice'), (2, 'Bob')");
try (ResultSet rs = st.executeQuery("SELECT id, name FROM demo ORDER BY id")) {
while (rs.next()) {
System.out.println(rs.getLong(1) + ": " + rs.getString(2));
}
}
}
}
}Some applications don't need JDBC APIs and prefer a small, explicit client that talks Hrana v3 over HTTP. For that, use HranaHttpClient and HranaHttpStream.
- When to choose this:
- You want minimal overhead and direct access to Hrana concepts (pipeline, baton, base_url stickiness).
- You are building your own repository/DAO layer or a serverless function where JDBC is inconvenient.
- When to prefer JDBC: if you have existing code that already uses
java.sql.*or you need the familiarConnection/PreparedStatement/ResultSetAPIs.
import com.bedatadriven.hrana.HranaHttpClient;
import com.bedatadriven.hrana.HranaHttpStream;
import com.bedatadriven.hrana.proto.Hrana.StmtResult;
import java.util.List;
import java.util.Map;
public class LowLevelDemo {
public static void main(String[] args) throws Exception {
String baseUrl = "https://mydb.turso.io/my_namespace"; // or normalize your libsql:// to https://
String jwt = System.getenv("HRANA_JWT");
HranaHttpClient client = new HranaHttpClient(baseUrl, jwt);
try (HranaHttpStream stream = client.newStream()) {
// Simple statements without result rows
stream.executeStatement("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)");
stream.executeStatement("DELETE FROM users");
// Positional parameters
stream.executePrepared(
"INSERT INTO users (name, age) VALUES (?, ?)",
List.of("Alice", 30)
);
// Named parameters (remember to include the prefix :/@/$ in the key)
stream.executePrepared(
"INSERT INTO users (name, age) VALUES (:name, :age)",
Map.of(":name", "Bob", ":age", 25)
);
// Query with rows
StmtResult result = stream.executePrepared(
"SELECT id, name, age FROM users WHERE age >= ? ORDER BY id",
List.of(20)
);
// Inspect columns and rows (protobuf model)
System.out.println("cols: " + result.getColsList().size());
System.out.println("rows: " + result.getRowsList().size());
// Autocommit state (true means not inside an explicit transaction)
boolean isAutocommit = stream.getAutocommit();
System.out.println("autocommit: " + isAutocommit);
// Transactions can be controlled with SQL
stream.executeStatement("BEGIN");
stream.executePrepared("INSERT INTO users (name, age) VALUES (?, ?)", List.of("Carol", 22));
stream.executeStatement("COMMIT");
}
}
}Notes for the low-level API:
- Streams are stateful over HTTP using a server-issued baton; the stream includes it automatically on subsequent requests.
- The server can return a new
base_urlfor stickiness; the stream updates its internal URL accordingly. - Requests are serialized per stream; do not issue concurrent calls on the same
HranaHttpStream. - Each call must contain exactly one SQL statement; multi-statement strings (with semicolons) are not supported by the protocol.
- For types, booleans are encoded as 0/1 integers; blobs use
byte[]. - Errors can be HTTP-level (non-2xx) or Hrana-level (returned in the protobuf body). Methods throw
IOExceptionfor both kinds. executeStatement(String)setswant_rows=falseand ignores rows; useexecutePrepared(sql, List)to obtain aStmtResultwith rows.
Supported URL format:
jdbc:libsql://host[:port][/namespace][?jwt=TOKEN]
JWT token resolution priority (first non-empty wins):
- URL query parameter
jwt Propertiespassed toDriverManager.getConnection(url, props)under key"jwt"- System property
HRANA_JWT - Environment variable
HRANA_JWT
Notes:
- The driver internally converts
libsql://...to the HTTPS base URL expected by the HTTP client. - Any trailing slash is trimmed automatically.
- Statement:
try (Statement st = conn.createStatement()) {
st.executeUpdate("INSERT INTO users (id, name) VALUES (1, 'Alice')");
try (ResultSet rs = st.executeQuery("SELECT id, name FROM users ORDER BY id")) {
while (rs.next()) {
long id = rs.getLong(1);
String name = rs.getString(2);
}
}
}- PreparedStatement with parameters and binary data:
try (PreparedStatement ps = conn.prepareStatement(
"INSERT INTO files (id, name, data) VALUES (?, ?, ?)") ) {
ps.setInt(1, 10);
ps.setString(2, "readme.txt");
ps.setBytes(3, new byte[]{1,2,3});
ps.executeUpdate();
}- Transactions:
conn.setAutoCommit(false);
try (PreparedStatement ps = conn.prepareStatement(
"INSERT INTO logs (id, msg) VALUES (?, ?)") ) {
ps.setLong(1, 200);
ps.setString(2, "Persisted");
ps.executeUpdate();
}
conn.commit();
conn.setAutoCommit(true);Integration tests require environment variables:
HRANA_URL: Base HTTPS URL (e.g.https://YOUR-DB.turso.io/your_nsorlibsql://...).HRANA_JWT: JWT token with permissions to create/modify tables.
Run:
HRANA_URL="https://YOUR-DB.turso.io/your_ns" \
HRANA_JWT="YOUR_TOKEN" \
./gradlew test --infoThe tests will normalize a libsql:// value to https:// automatically.
- Not a full JDBC-compliant driver (
Driver#jdbcCompliant()returns false) - No
CallableStatement - Limited type mapping; focused on common SQLite/LibSQL types
- Only Hrana over HTTP v3 protobuf pipeline is supported
- "No suitable driver" — ensure this JAR is on the classpath; the service file
META-INF/services/java.sql.Drivermust be present. You can also force load with:Class.forName("com.bedatadriven.hrana.HranaDriver");
- Auth errors — verify your JWT and that it has privileges for the target namespace.
- Endpoint URL — ensure you use your database host and namespace; trailing slashes are fine but will be trimmed.