Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ internal class SQLiteJournalModeDatabaseBenchmark(inputs: Inputs) {

@OptIn(Experimental::class)
@Test
fun batchInsertInt(): Unit = databaseHelper.writableDatabase.run {
fun batchSequenceInsertInt(): Unit = databaseHelper.writableDatabase.run {
val args = Array(1) { 0 }
benchmarkRule.measureRepeated {
batch("INSERT OR REPLACE INTO 'Foo' VALUES (?)", sequence {
Expand All @@ -206,6 +206,17 @@ internal class SQLiteJournalModeDatabaseBenchmark(inputs: Inputs) {
}
}

@OptIn(Experimental::class)
@Test
fun batchArrayInsertInt(): Unit = databaseHelper.writableDatabase.run {
val bindArgs = Array(100) {
arrayOf(it)
}
benchmarkRule.measureRepeated {
batch("INSERT OR REPLACE INTO 'Foo' VALUES (?)", bindArgs)
}
}

@Test
fun queryAndInsertIntWithOnConflict(): Unit = databaseHelper.writableDatabase.run {
val values = ContentValues()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,21 @@ class SQLiteDatabase private constructor(
}
}

/**
* Transacts to the database in exclusive mode a batch of queries with the same underlying SQL statement. The
* prototypical use case is for database modifications inside a tight loop to which this is optimised.
*
* Each sub-array must have the same length, corresponding to the number of arguments in the SQL statement.
* Each sub-array must have the same types at corresponding indices.
*
* @param sql statement with ? placeholders for bind parameters.
* @param bindArgs arrays of arguments for binding to the statement; each sub-array must have the same types
* at corresponding indices.
* @return the number of rows affected.
*/
@Experimental
fun batch(@Language("RoomSql") sql: String, bindArgs: Array<out Array<*>>): Int = database.batch(sql, bindArgs)

/**
* Transacts to the database in exclusive mode a batch of queries with the same underlying SQL statement. The
* prototypical use case is for database modifications inside a tight loop to which this is optimised.
Expand All @@ -171,6 +186,23 @@ class SQLiteDatabase private constructor(
@Experimental
fun batch(@Language("RoomSql") sql: String, bindArgs: Sequence<Array<out Any?>>): Int = database.batch(sql, bindArgs)

/**
* Transacts to the database in exclusive mode a batch of queries with the same underlying SQL statement. The
* prototypical use case is for database modifications inside a tight loop to which this is optimised.
*
* Each array in the iterable must have the same length, corresponding to the number of arguments in the SQL statement.
* It is safe for the iterable to recycle the array with each step.
*
* The transaction is not committed by this method until the iterable is exhausted. For long sequences you may therefore
* wish to yield the transaction periodically.
*
* @param sql statement with ? placeholders for bind parameters.
* @param bindArgs sequence of standard type arguments for binding to the statement.
* @return the number of rows affected.
*/
@Experimental
fun batch(@Language("RoomSql") sql: String, bindArgs: Iterable<Array<out Any?>>): Int = database.batch(sql, bindArgs)

/**
* Begins a transaction in exclusive mode. Prefer [transact] whenever possible.
*
Expand Down
9 changes: 8 additions & 1 deletion selekt-java/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ sourceSets {
runtimeClasspath += sourceSets.main.get().output
resources.srcDir(layout.buildDirectory.dir("intermediates/libs"))
}
named("jmh") {
resources.srcDir(layout.buildDirectory.dir("intermediates/libs"))
}
}

val integrationTestImplementation: Configuration by configurations.getting {
Expand All @@ -58,7 +61,6 @@ val integrationTestRuntimeOnly: Configuration by configurations.getting {
dependencies {
implementation(projects.selektApi)
implementation(projects.selektSqlite3Classes)
jmhImplementation(libs.kotlinx.coroutines.core)
}

jmh {
Expand All @@ -67,6 +69,11 @@ jmh {
}
}

tasks.named("jmh") {
dependsOn("buildHostSQLite")
shouldRunAfter("test", "integrationTest")
}

publishing {
publications.register<MavenPublication>("main") {
from(components.getByName("java"))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
/*
* Copyright 2025 Bloomberg Finance L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.bloomberg.selekt.batch.benchmarks;

import com.bloomberg.selekt.CommonThreadLocalRandom;
import com.bloomberg.selekt.commons.DatabaseKt;
import com.bloomberg.selekt.SQLDatabase;
import com.bloomberg.selekt.SQLiteJournalMode;
import com.bloomberg.selekt.jvm.SQLite;
import kotlin.sequences.Sequence;
import kotlin.sequences.SequencesKt;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.TearDown;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.annotations.OutputTimeUnit;

import java.io.File;
import java.util.concurrent.TimeUnit;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Random;

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 10, time = 3, timeUnit = TimeUnit.SECONDS)
@Fork(value = 1)
@State(Scope.Benchmark)
public class BatchMethodBenchmark {
@Param({"5000", "10000", "25000"})
public int batchSize;

@Param({"SIMPLE", "MIXED", "LARGE_BLOBS"})
public String dataType;

private File databaseFile;
private SQLDatabase database;
private Object[][] arrayBatchData;
private Sequence<Object[]> sequenceBatchData;
private Object[][] lazyArrayData;
private Sequence<Object[]> lazySequenceData;
private Object[][] complexArrayData;
private Sequence<Object[]> complexSequenceData;
private Object[][] updateData;
private Sequence<Object[]> updateSequenceData;
private final Random random = new Random(42);

private static final String SQL_INSERT = "INSERT OR REPLACE INTO batch_test (id, name, value, data) " +
"VALUES (?, ?, ?, ?)";

@Setup(Level.Iteration)
public void setup() throws IOException {
databaseFile = Files.createTempFile("benchmark-batch", ".db").toFile();
database = new SQLDatabase(
databaseFile.getAbsolutePath(),
SQLite.INSTANCE,
SQLiteJournalMode.WAL.databaseConfiguration,
null,
CommonThreadLocalRandom.INSTANCE
);
database.exec("DROP TABLE IF EXISTS batch_test", null);
database.exec("CREATE TABLE batch_test (" +
"id INTEGER PRIMARY KEY, " +
"name TEXT, " +
"value REAL, " +
"data BLOB" +
")", null);
generateTestData();
}

@TearDown(Level.Iteration)
public void tearDown() {
database.close();
DatabaseKt.deleteDatabase(databaseFile);
}

private void generateTestData() {
arrayBatchData = new Object[batchSize][];
for (int i = 0; i < batchSize; i++) {
arrayBatchData[i] = createRowData(i);
}
sequenceBatchData = SequencesKt.sequenceOf(arrayBatchData);

lazyArrayData = new Object[batchSize][];
for (int i = 0; i < batchSize; i++) {
lazyArrayData[i] = new Object[]{
i,
"lazy_" + i,
i * Math.E,
(i % 10 == 0) ? generateRandomBytes(128) : generateRandomBytes(32)
};
}
lazySequenceData = SequencesKt.sequenceOf(lazyArrayData);

complexArrayData = new Object[batchSize][];
for (int i = 0; i < batchSize; i++) {
switch (i % 4) {
case 0:
complexArrayData[i] = new Object[]{i, "type_a", i * 1.0, generateRandomBytes(8)};
break;
case 1:
complexArrayData[i] = new Object[]{i, "type_b", i * 2.0, generateRandomBytes(32)};
break;
case 2:
complexArrayData[i] = new Object[]{i, "type_c", i * 3.0, generateRandomBytes(64)};
break;
default:
complexArrayData[i] = new Object[]{i, "type_d", i * 0.5, generateRandomBytes(16)};
break;
}
}
complexSequenceData = SequencesKt.sequenceOf(complexArrayData);

updateData = new Object[batchSize][];
for (int i = 0; i < batchSize; i++) {
updateData[i] = new Object[]{
"updated_" + i,
i * 10.0,
i
};
}
updateSequenceData = SequencesKt.sequenceOf(updateData);
}

private Object[] createRowData(int index) {
return switch (dataType) {
case "SIMPLE" -> new Object[]{
index,
"item_" + index,
index * 1.5,
null
};
case "MIXED" -> new Object[]{
index,
(index % 3 == 0) ? "mixed_" + index : "default_" + index,
(index % 2 == 0) ? index * 2.5 : 0.0,
(index % 5 == 0) ? generateRandomBytes(64) : generateRandomBytes(16)
};
case "LARGE_BLOBS" -> new Object[]{
index,
"blob_item_" + index,
index * 1.0,
generateRandomBytes(2048)
};
default -> new Object[]{index, "default", 1.0, null};
};
}

private byte[] generateRandomBytes(int size) {
byte[] bytes = new byte[size];
random.nextBytes(bytes);
return bytes;
}

@Benchmark
public int arrayBatchMethod() {
return database.batch(SQL_INSERT, arrayBatchData);
}

@Benchmark
public int sequenceBatchMethod() {
return database.batch(SQL_INSERT, sequenceBatchData);
}

@Benchmark
public int arrayBatchUpdate() {
database.batch(SQL_INSERT, arrayBatchData);
return database.batch(
"UPDATE batch_test SET name = ?, value = ? WHERE id = ?",
updateData
);
}

@Benchmark
public int sequenceBatchUpdate() {
database.batch(SQL_INSERT, sequenceBatchData);
return database.batch(
"UPDATE batch_test SET name = ?, value = ? WHERE id = ?",
updateSequenceData
);
}

@Benchmark
public int arrayBatchLazyEquivalent() {
return database.batch(SQL_INSERT, lazyArrayData);
}

@Benchmark
public int sequenceBatchLazy() {
return database.batch(SQL_INSERT, lazySequenceData);
}

@Benchmark
public int arrayBatchComplex() {
return database.batch(SQL_INSERT, complexArrayData);
}

@Benchmark
public int sequenceBatchComplex() {
return database.batch(SQL_INSERT, complexSequenceData);
}
}
27 changes: 27 additions & 0 deletions selekt-java/src/jmh/java/com/bloomberg/selekt/jvm/SQLite.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2020 Bloomberg Finance L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.bloomberg.selekt.jvm

import com.bloomberg.selekt.SQLite
import com.bloomberg.selekt.commons.loadEmbeddedLibrary
import com.bloomberg.selekt.externalSQLiteSingleton

private val sqlite = externalSQLiteSingleton {
loadEmbeddedLibrary(SQLite::class.java.classLoader, "jni", "selekt")
}

internal object SQLite : SQLite(sqlite)
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,16 @@
package com.bloomberg.selekt

internal interface BatchSQLExecutor {
fun executeForChangedRowCount(sql: String, bindArgs: Sequence<Array<out Any?>>): Int
/**
* @param sql SQL statement with ? placeholders for bind parameters.
* @param bindArgs arrays of arguments for binding to the statement; all sub-arrays must have
* the same length and the same types at corresponding indices (e.g., String, Int, ByteArray, null).
* @return the number of rows affected.
*/
fun executeBatchForChangedRowCount(sql: String, bindArgs: Array<out Array<out Any?>>): Int

fun executeBatchForChangedRowCount(sql: String, bindArgs: Sequence<Array<out Any?>>): Int

fun executeBatchForChangedRowCount(sql: String, bindArgs: Iterable<Array<out Any?>>): Int =
executeBatchForChangedRowCount(sql, bindArgs.asSequence())
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ class SQLDatabase(
SQLStatement.execute(session, sql, bindArgs)
}

fun batch(sql: String, bindArgs: Iterable<Array<out Any?>>): Int = transact {
SQLStatement.execute(session, sql, bindArgs)
}

fun batch(sql: String, bindArgs: Array<out Array<*>>): Int = transact {
SQLStatement.execute(session, sql, bindArgs)
}

override fun beginExclusiveTransaction() = pledge { session.get().beginExclusiveTransaction() }

override fun beginExclusiveTransactionWithListener(listener: SQLTransactionListener) = pledge {
Expand Down
Loading
Loading