JDBC
Integration
Selekt JDBC supports Java 11 and later. For new projects, we recommend the current LTS release (Java 25 at the time of writing) which uses Selekt's FFM backend. Java 11–24 remain supported through the JNI backend for compatibility. Gradle uses published variant metadata to select the appropriate backend from the project's target JVM version, not the JVM running Gradle.
Maven does not consume Gradle variant metadata. Maven consumers therefore receive the unclassified Java 11 JNI backend on every supported Java version, including Java 25 or later. FFM backend selection is not currently supported for Maven consumers.
Gradle
repositories {
mavenCentral()
}
dependencies {
implementation(platform("com.bloomberg.selekt:selekt-bom:1.6.8"))
implementation("com.bloomberg.selekt:selekt-jdbc")
}
repositories {
mavenCentral()
}
dependencies {
implementation platform('com.bloomberg.selekt:selekt-bom:1.6.8')
implementation 'com.bloomberg.selekt:selekt-jdbc'
}
Maven
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.bloomberg.selekt</groupId>
<artifactId>selekt-bom</artifactId>
<version>1.6.8</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.bloomberg.selekt</groupId>
<artifactId>selekt-jdbc</artifactId>
</dependency>
</dependencies>
Published artifacts include CycloneDX SBOMs. See Software bill of materials for publication, runtime-inspection and generation details.
Getting a connection
Using a DataSource
val dataSource = SelektDataSource().apply {
databasePath = "/path/to/database.db"
journalMode = "WAL" // is the default
busyTimeout = 2_500 // milliseconds is the default
maxPoolSize = 4 // is the default, with 3 read connections
cursorWindowSize = 1_024 // rows; the default
cursorWindowByteSize = 2 * 1024 * 1024 // bytes; the default
foreignKeys = true
}
dataSource.connection.use { connection ->
// Use connection
}
final SelektDataSource dataSource = new SelektDataSource();
dataSource.setDatabasePath("/path/to/database.db");
dataSource.setJournalMode("WAL"); // is the default
dataSource.setBusyTimeout(2500); // milliseconds is the default
dataSource.setMaxPoolSize(4); // is the default, with 3 read connections
dataSource.setCursorWindowSize(1024); // rows; the default
dataSource.setCursorWindowByteSize(2 * 1024 * 1024); // bytes; the default
dataSource.setForeignKeys(true);
try (Connection connection = dataSource.getConnection()) {
// Use connection
}
Using DriverManager
val url = "jdbc:sqlite:/path/to/database.db"
val connection = DriverManager.getConnection(url)
final String url = "jdbc:sqlite:/path/to/database.db";
final Connection connection = DriverManager.getConnection(url);
Connection properties can be passed via a Properties object:
val properties = Properties().apply {
setProperty("journalMode", "WAL")
setProperty("busyTimeout", "2500")
setProperty("poolSize", "4")
setProperty("cursorWindowSize", "1024")
setProperty("cursorWindowByteSize", "2097152")
setProperty("foreignKeys", "true")
}
val connection = DriverManager.getConnection(
"jdbc:sqlite:/path/to/database.db",
properties
)
final Properties properties = new Properties();
properties.setProperty("journalMode", "WAL");
properties.setProperty("busyTimeout", "2500");
properties.setProperty("poolSize", "4");
properties.setProperty("cursorWindowSize", "1024");
properties.setProperty("cursorWindowByteSize", "2097152");
properties.setProperty("foreignKeys", "true");
final Connection connection = DriverManager.getConnection(
"jdbc:sqlite:/path/to/database.db",
properties
);
Properties can also be inlined in the URL query string:
val connection = DriverManager.getConnection(
"jdbc:sqlite:/path/to/database.db?journalMode=WAL&busyTimeout=2500&poolSize=4" +
"&cursorWindowSize=1024&cursorWindowByteSize=2097152&foreignKeys=true"
)
final Connection connection = DriverManager.getConnection(
"jdbc:sqlite:/path/to/database.db?journalMode=WAL&busyTimeout=2500&poolSize=4" +
"&cursorWindowSize=1024&cursorWindowByteSize=2097152&foreignKeys=true"
);
Encryption
Selekt uses SQLCipher for AES-256 encryption. Encryption is opt-in, databases are unencrypted by default. To enable encryption, provide a key that is exactly 32 bytes. Selekt treats these bytes as raw key material and does not apply PBKDF2 or another password-based key derivation function. Do not supply a human-readable password or passphrase; use cryptographically random bytes or the 32-byte output of a suitable key derivation function.
Represent arbitrary key bytes as a CharArray containing 0x followed by exactly 64 hexadecimal digits. Selekt also accepts a non-prefixed CharArray, but encodes it as UTF-8; its encoded length, rather than its character count, must be exactly 32 bytes.
SelektDriver does not accept encryption keys. Encrypted connections must use SelektDataSource.setEncryption with an EncryptionKeySource.Literal backed by a caller-owned CharArray. SelektDataSource stores and later zeroes an internal copy; zero the caller-owned array after setEncryption returns.
With a DataSource
private fun deriveHexEncodedKey(): CharArray = TODO(
"Return 32 bytes from a cryptographically secure random-number generator or suitable " +
"key derivation function, encoded as '0x' followed by 64 hexadecimal digits.")
val dataSource = SelektDataSource().apply {
databasePath = "/path/to/encrypted.db"
}
val key = deriveHexEncodedKey()
try {
dataSource.setEncryption(EncryptionKeySource.Literal(key))
} finally {
key.fill('\u0000')
}
private char[] deriveHexEncodedKey() {
// TODO Return 32 bytes from a cryptographically secure random-number generator or suitable
// key derivation function, encoded as "0x" followed by 64 hexadecimal digits.
}
final SelektDataSource dataSource = new SelektDataSource();
dataSource.setDatabasePath("/path/to/encrypted.db");
final char[] key = deriveHexEncodedKey();
try {
dataSource.setEncryption(new EncryptionKeySource.Literal(key));
} finally {
java.util.Arrays.fill(key, '\0');
}
Interaction
Querying with a PreparedStatement
dataSource.connection.use { connection ->
connection.prepareStatement(
"SELECT id, name FROM users WHERE id = ?"
).use { statement ->
statement.setInt(1, 42)
statement.executeQuery().use { resultSet ->
while (resultSet.next()) {
println(
"${resultSet.getInt("id")}: ${resultSet.getString("name")}"
)
}
}
}
}
try (Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement(
"SELECT id, name FROM users WHERE id = ?")) {
statement.setInt(1, 42);
try (ResultSet resultSet = statement.executeQuery()) {
while (resultSet.next()) {
System.out.println(
resultSet.getInt("id") + ": " + resultSet.getString("name")
);
}
}
}
Cursor memory and scrolling
Scrollable JVM cursors retain at most 1,024 rows and an estimated 2 MiB by default. Moving outside the retained window
re-runs the query to refill it, so request ResultSet.TYPE_SCROLL_SENSITIVE. Selekt does not advertise or accept
TYPE_SCROLL_INSENSITIVE; use a transaction when scrolling must observe a stable snapshot. A row whose estimated size
exceeds 2 MiB is rejected rather than copied into the cursor window.
Auto-commit TYPE_FORWARD_ONLY result sets—the JDBC default—stream rows and do not use these window limits. Selekt may
materialise a forward-only query inside a manual read-only transaction to avoid pinning its SQLite snapshot after the
query call.
Inserting data
dataSource.connection.use { connection ->
connection.prepareStatement(
"INSERT INTO users (id, name) VALUES (?, ?)"
).use { statement ->
statement.setInt(1, 1)
statement.setString(2, "Alice")
statement.executeUpdate()
}
}
try (Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement(
"INSERT INTO users (id, name) VALUES (?, ?)")) {
statement.setInt(1, 1);
statement.setString(2, "Alice");
statement.executeUpdate();
}
Batch inserts
dataSource.connection.use { connection ->
connection.autoCommit = false
try {
connection.prepareStatement(
"INSERT INTO users (id, name) VALUES (?, ?)"
).use { statement ->
for (i in 1..1000) {
statement.setInt(1, i)
statement.setString(2, "User $i")
statement.addBatch()
}
statement.executeBatch()
}
connection.commit()
} catch (e: SQLException) {
connection.rollback()
throw e
}
}
try (Connection connection = dataSource.getConnection()) {
connection.setAutoCommit(false);
try (PreparedStatement statement = connection.prepareStatement(
"INSERT INTO users (id, name) VALUES (?, ?)"
)) {
for (int i = 1; i <= 1000; i++) {
statement.setInt(1, i);
statement.setString(2, "User " + i);
statement.addBatch();
}
statement.executeBatch();
connection.commit();
} catch (final SQLException e) {
connection.rollback();
throw e;
}
}
Selekt may reuse the driver-owned update-count array returned by executeBatch(). Treat the array as read-only and copy it before another batch execution or before closing the statement if the counts need to be retained.
Transactions
dataSource.connection.use { connection ->
connection.autoCommit = false
try {
connection.prepareStatement(
"UPDATE accounts SET balance = balance - ? WHERE id = ?"
).use { statement ->
statement.setDouble(1, 100.0)
statement.setInt(2, 1)
statement.executeUpdate()
}
connection.prepareStatement(
"UPDATE accounts SET balance = balance + ? WHERE id = ?"
).use { statement ->
statement.setDouble(1, 100.0)
statement.setInt(2, 2)
statement.executeUpdate()
}
connection.commit()
} catch (e: SQLException) {
connection.rollback()
throw e
}
}
try (Connection connection = dataSource.getConnection()) {
connection.setAutoCommit(false);
try {
try (final PreparedStatement statement = connection.prepareStatement(
"UPDATE accounts SET balance = balance - ? WHERE id = ?"
)) {
statement.setDouble(1, 100.0);
statement.setInt(2, 1);
statement.executeUpdate();
}
try (final PreparedStatement statement = connection.prepareStatement(
"UPDATE accounts SET balance = balance + ? WHERE id = ?"
)) {
statement.setDouble(1, 100.0);
statement.setInt(2, 2);
statement.executeUpdate();
}
connection.commit();
} catch (final SQLException e) {
connection.rollback();
throw e;
}
}
Connection properties
| Property | Type | Default | Description |
|---|---|---|---|
journalMode |
String | WAL |
SQLite journal mode (DELETE, TRUNCATE, PERSIST, MEMORY, WAL, OFF) |
busyTimeout |
int | 2500 |
SQLite busy timeout in milliseconds |
poolSize |
int | 4 |
Maximum connection pool size |
foreignKeys |
boolean | true |
Enable foreign key constraints |
Closing the DataSource
dataSource.close()
dataSource.close();
Calling close() releases all pooled connections and zeroes any encryption key material. The method is idempotent.