Java Driver
High-performance Java driver for Stoolap. Built with jni-rs to call the Rust API directly (no C FFI layer). Ships with a full JDBC driver so it works with any standard Java SQL code, HikariCP, JPA, and other JDBC tooling.
Installation
Until the first release is published to Maven Central, build from source:
git clone https://github.com/stoolap/stoolap-java.git
cd stoolap-java
# 1. Build the native library (requires Rust)
cd jni && cargo build --release && cd ..
# 2. Build the Java artifact
mvn package
# 3. Install to your local Maven repo
mvn install
In your pom.xml:
<dependency>
<groupId>io.stoolap</groupId>
<artifactId>stoolap-java</artifactId>
<version>0.4.0</version>
</dependency>
Requires Java 17+ (tested on Java 17, 21, 25). Supported platforms: macOS (aarch64/x86_64), Linux (x86_64/aarch64), Windows (x86_64).
Native Library Loading
The driver searches for libstoolap_jni.{dylib,so,dll} in this order:
- Path in
STOOLAP_LIBenvironment variable - Bundled JAR resource at
/native/{os}-{arch}/libstoolap_jni.{ext} - System library path (
java.library.path)
export STOOLAP_LIB=/path/to/stoolap-java/jni/target/release/libstoolap_jni.dylib
java -cp stoolap-java-0.4.0.jar:your-app.jar your.Main
Quick Start
import io.stoolap.StoolapDB;
import io.stoolap.internal.BulkDecoder;
try (StoolapDB db = StoolapDB.openInMemory()) {
db.execute("""
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT
)
""");
// Insert with positional parameters ($1, $2, ...)
db.execute(
"INSERT INTO users (id, name, email) VALUES ($1, $2, $3)",
1L, "Alice", "alice@example.com"
);
// Query rows — result contains column names and row data
BulkDecoder.Result users = db.query("SELECT * FROM users ORDER BY id");
for (Object[] row : users.rows()) {
long id = (Long) row[0];
String name = (String) row[1];
String email = (String) row[2];
System.out.println(id + " " + name + " " + email);
}
}
Opening a Database
// In-memory (isolated, each call creates a new instance)
StoolapDB db = StoolapDB.openInMemory();
// In-memory via DSN
StoolapDB db = StoolapDB.open("memory://");
// File-based (data persists across restarts)
StoolapDB db = StoolapDB.open("file:///absolute/path/to/db");
Core API Methods
| Method | Returns | Description |
|---|---|---|
execute(sql) |
long |
Execute DDL/DML statement, returns rows affected |
execute(sql, params...) |
long |
Execute with positional parameters |
query(sql) |
BulkDecoder.Result |
Execute query, returns all rows |
query(sql, params...) |
BulkDecoder.Result |
Query with positional parameters |
prepare(sql) |
StoolapStmt |
Create a prepared statement |
begin() |
StoolapTx |
Begin a transaction (READ COMMITTED) |
begin(boolean snapshot) |
StoolapTx |
Begin with SNAPSHOT isolation if true |
cloneHandle() |
StoolapDB |
Clone for multi-threaded use |
close() |
void |
Close the database |
BulkDecoder.Result
query() returns a BulkDecoder.Result record with:
public record Result(String[] columnNames, List<Object[]> rows) {
int getColumnCount();
int getRowCount();
}
Each row is an Object[] containing typed values. See Type Mapping for how SQL types map to Java types.
Persistence
File-based databases persist data to disk using WAL (Write-Ahead Logging) and immutable cold volumes. A background checkpoint cycle seals hot rows into columnar volume files, compacts them, and truncates the WAL. Data survives process restarts.
try (StoolapDB db = StoolapDB.open("file:///tmp/mydata")) {
db.execute("CREATE TABLE kv (key TEXT PRIMARY KEY, value TEXT)");
db.execute("INSERT INTO kv VALUES ($1, $2)", "hello", "world");
}
// Reopen: data is still there
try (StoolapDB db = StoolapDB.open("file:///tmp/mydata")) {
BulkDecoder.Result result = db.query("SELECT * FROM kv WHERE key = $1", "hello");
System.out.println(result.rows().get(0)[1]); // "world"
}
Configuration
Pass configuration as query parameters in the DSN:
// Maximum durability (fsync on every WAL write)
StoolapDB db = StoolapDB.open("file:///tmp/mydata?sync_mode=full");
// Custom checkpoint interval with compression
StoolapDB db = StoolapDB.open(
"file:///tmp/mydata?checkpoint_interval=60&wal_compression=on&volume_compression=on"
);
// Multiple options
StoolapDB db = StoolapDB.open(
"file:///tmp/mydata?sync_mode=full&checkpoint_interval=120&compact_threshold=8"
);
All configuration options from the core engine are supported (see the Python driver for the full table — the same keys work here).
Prepared Statements
Prepared statements parse SQL once and reuse the cached execution plan on every call. The Java driver stores a CachedPlanRef internally, so execute/query bypass SQL parsing and plan-cache lookup entirely.
try (StoolapDB db = StoolapDB.openInMemory()) {
db.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)");
try (StoolapStmt insert = db.prepare("INSERT INTO users VALUES ($1, $2, $3)")) {
insert.execute(1L, "Alice", "alice@example.com");
insert.execute(2L, "Bob", "bob@example.com");
}
try (StoolapStmt lookup = db.prepare("SELECT * FROM users WHERE id = $1")) {
BulkDecoder.Result result = lookup.query(1L);
if (result.getRowCount() > 0) {
Object[] row = result.rows().get(0);
System.out.println("Found: " + row[1]);
}
}
}
StoolapStmt Methods
| Method | Returns | Description |
|---|---|---|
execute(params...) |
long |
Execute DDL/DML, returns rows affected |
query(params...) |
BulkDecoder.Result |
Execute query |
getSql() |
String |
Return the original SQL text |
close() |
void |
Finalize the native handle |
Transactions
try (StoolapDB db = StoolapDB.openInMemory()) {
db.execute("CREATE TABLE accounts (id INTEGER, balance INTEGER)");
db.execute("INSERT INTO accounts VALUES (1, 100)");
db.execute("INSERT INTO accounts VALUES (2, 0)");
try (StoolapTx tx = db.begin()) {
tx.execute("UPDATE accounts SET balance = balance - 50 WHERE id = 1");
tx.execute("UPDATE accounts SET balance = balance + 50 WHERE id = 2");
tx.commit();
// Auto-rollback on close if commit wasn't called
}
}
Snapshot Isolation
try (StoolapTx tx = db.begin(true /* snapshot */)) {
// Sees a consistent view from the transaction's start point.
// Writes from other connections are invisible until commit.
BulkDecoder.Result snapshot = tx.query("SELECT * FROM t");
tx.commit();
}
Transaction Methods
| Method | Returns | Description |
|---|---|---|
execute(sql, params...) |
long |
Execute DDL/DML |
query(sql, params...) |
BulkDecoder.Result |
Execute query |
execute(stmt, params...) |
long |
Execute a prepared statement within the transaction |
query(stmt, params...) |
BulkDecoder.Result |
Query a prepared statement within the transaction |
commit() |
void |
Commit the transaction |
rollback() |
void |
Rollback the transaction |
close() |
void |
Auto-rollback if not committed |
Multi-Threaded Use
A single StoolapDB handle must not be used concurrently from multiple threads. Use cloneHandle() to create a per-thread handle that shares the underlying engine (data, indexes, transactions):
StoolapDB main = StoolapDB.open("file:///tmp/mydata");
Runnable worker = () -> {
try (StoolapDB local = main.cloneHandle()) {
BulkDecoder.Result r = local.query("SELECT COUNT(*) FROM t");
System.out.println(r.rows().get(0)[0]);
} catch (Exception e) {
e.printStackTrace();
}
};
Thread t1 = new Thread(worker);
Thread t2 = new Thread(worker);
t1.start(); t2.start();
t1.join(); t2.join();
main.close();
Cloning is cheap and integrates naturally with JDBC connection pools like HikariCP.
JDBC Driver
Full java.sql implementation for drop-in compatibility with existing JDBC code.
Registration
The driver auto-registers via the META-INF/services/java.sql.Driver SPI. Just use the URL:
import java.sql.*;
try (Connection conn = DriverManager.getConnection("jdbc:stoolap:memory://");
Statement stmt = conn.createStatement()) {
stmt.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)");
stmt.executeUpdate("INSERT INTO users VALUES (1, 'Alice')");
try (ResultSet rs = stmt.executeQuery("SELECT id, name FROM users")) {
while (rs.next()) {
System.out.println(rs.getInt("id") + " " + rs.getString("name"));
}
}
}
JDBC URL Formats
| URL | Description |
|---|---|
jdbc:stoolap:memory:// |
In-memory database |
jdbc:stoolap:file:///path/to/db |
File-based |
jdbc:stoolap:file:///path/to/db?sync_mode=full |
File-based with DSN options |
PreparedStatement
try (Connection conn = DriverManager.getConnection("jdbc:stoolap:memory://")) {
conn.createStatement().execute("CREATE TABLE t (id INTEGER, name TEXT, score FLOAT)");
try (PreparedStatement ps = conn.prepareStatement("INSERT INTO t VALUES ($1, $2, $3)")) {
ps.setLong(1, 1);
ps.setString(2, "Alice");
ps.setDouble(3, 95.5);
ps.executeUpdate();
}
}
Batch Execution
The JDBC batch path auto-wraps in a transaction and uses execute_prepared on the cached plan AST, so per-row overhead is minimal.
try (Connection conn = DriverManager.getConnection("jdbc:stoolap:memory://")) {
conn.createStatement().execute("CREATE TABLE t (id INTEGER, name TEXT)");
try (PreparedStatement ps = conn.prepareStatement("INSERT INTO t VALUES ($1, $2)")) {
for (int i = 0; i < 1000; i++) {
ps.setLong(1, i);
ps.setString(2, "item_" + i);
ps.addBatch();
}
ps.executeBatch();
}
}
Transaction Control via JDBC
try (Connection conn = DriverManager.getConnection("jdbc:stoolap:memory://")) {
conn.setAutoCommit(false);
try (Statement stmt = conn.createStatement()) {
stmt.executeUpdate("INSERT INTO t VALUES (1, 'a')");
stmt.executeUpdate("INSERT INTO t VALUES (2, 'b')");
conn.commit();
} catch (SQLException e) {
conn.rollback();
throw e;
} finally {
conn.setAutoCommit(true);
}
}
Connection Pool Integration
StoolapConnection clones the underlying handle, making it safe to pool. Example with HikariCP:
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:stoolap:file:///var/data/mydb");
config.setMaximumPoolSize(10);
try (HikariDataSource ds = new HikariDataSource(config);
Connection conn = ds.getConnection()) {
// ...
}
Type Mapping
| Java (write) | Stoolap | Java (read) |
|---|---|---|
Long, Integer, Short, Byte |
INTEGER |
Long |
Double, Float, BigDecimal |
FLOAT |
Double |
String |
TEXT |
String |
Boolean |
BOOLEAN |
Boolean |
java.time.Instant, java.sql.Timestamp |
TIMESTAMP |
java.time.Instant |
String (JSON) |
JSON |
String |
byte[] |
BLOB |
byte[] |
null |
NULL |
null |
Aggregate results (SUM, AVG, MIN, MAX over integer columns) may be returned as Double depending on the engine’s promotion rules. Use ((Number) value).longValue() when reading aggregate output.
Parameters
The Java driver currently supports positional parameters only ($1, $2, …):
db.query("SELECT * FROM users WHERE id = $1 AND name = $2", 1L, "Alice");
db.execute("INSERT INTO t VALUES ($1, $2, $3)", 42L, "hello", 3.14);
Params are encoded to a compact binary format on the Java side by ParamEncoder and decoded in Rust without any JNI reflection, keeping the per-call overhead minimal.
Error Handling
The core API throws StoolapException, which extends java.sql.SQLException for JDBC compatibility:
import io.stoolap.StoolapDB;
import io.stoolap.StoolapException;
try (StoolapDB db = StoolapDB.openInMemory()) {
db.execute("CREATE TABLE t (id INTEGER PRIMARY KEY)");
db.execute("INSERT INTO t VALUES (1)");
db.execute("INSERT INTO t VALUES (1)"); // duplicate PK
} catch (StoolapException e) {
System.err.println("Database error: " + e.getMessage());
}
JDBC code catches SQLException the normal way.
Architecture
+------------------------------------------------------+
| Your Java application |
+------------------------------------------------------+
| java.sql.* (JDBC) | io.stoolap.* (core) |
+------------------------------------------------------+
| io.stoolap.internal |
| +-- NativeBridge (static native methods) |
| +-- ParamEncoder (Java -> byte[]) |
| +-- BulkDecoder (byte[] -> Object[]) |
+------------------------------------------------------+
| JNI (jni-rs) |
v v
+------------------------------------------------------+
| stoolap-jni (Rust crate in jni/) |
| +-- Wraps stoolap::api::{Database, Statement, Tx} |
| +-- CachedPlanRef for zero-parse execution |
| +-- encode_rows / decode_binary_params |
+------------------------------------------------------+
|
v
+------------------------------------------------------+
| stoolap crate (Rust) |
| MVCC, columnar indexes, volume storage, WAL |
+------------------------------------------------------+
Query results are encoded once in Rust and transferred to Java as a single byte[], which is decoded lazily by BulkDecoder. This eliminates per-row JNI crossings, which is the main performance bottleneck in naive JNI drivers.
Building from Source
Requires:
- Rust (stable)
- Java 17+ (Java 21 LTS or later recommended)
- Maven 3.9+
git clone https://github.com/stoolap/stoolap-java.git
cd stoolap-java
# Build the native library
cd jni && cargo build --release && cd ..
# Build, test, and install to local Maven repo
export STOOLAP_LIB=$(pwd)/jni/target/release/libstoolap_jni.dylib # or .so / .dll
mvn verify
Run the full benchmark against SQLite JDBC:
java -Djava.library.path=jni/target/release \
-cp "target/classes:target/test-classes:$(mvn dependency:build-classpath -q -DincludeScope=test -Dmdep.outputFile=/dev/stdout)" \
io.stoolap.Benchmark