JDBC protocol plugin for Gatling load testing framework. Execute SQL queries, inserts, updates, batch operations, raw SQL, and stored procedures against any JDBC-compatible database with connection pooling (HikariCP) and result checks.
- Compatibility
- Installation
- Database Driver Dependencies
- Quick Start
- Protocol Configuration
- Actions
- Checks
- Session Variables
- Upgrading to 1.5.0
- Examples
- Contributing
- License
| Plugin Version | Gatling | Scala | Java |
|---|---|---|---|
| 1.x.y | 3.13.x | 2.13 | 11+ |
| 0.20.x | 3.13.x | 2.13 | 17+ |
Branch strategy:
maintargets Gatling 3.13.x.
libraryDependencies += "org.galaxio" %% "gatling-jdbc-plugin" % "<version>" % Testgatling("org.galaxio:gatling-jdbc-plugin_2.13:<version>")<dependency>
<groupId>org.galaxio</groupId>
<artifactId>gatling-jdbc-plugin_2.13</artifactId>
<version>${version}</version>
<scope>test</scope>
</dependency>This plugin does not bundle vendor JDBC drivers. You must add the driver for your database separately.
// sbt
libraryDependencies += "org.postgresql" % "postgresql" % "42.7.11" % Test// Gradle Kotlin DSL
gatling("org.postgresql:postgresql:42.7.11")<!-- Maven -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.11</version>
<scope>test</scope>
</dependency>libraryDependencies += "com.mysql" % "mysql-connector-j" % "9.3.0" % TestlibraryDependencies += "com.microsoft.sqlserver" % "mssql-jdbc" % "12.10.0.jre11" % TestlibraryDependencies += "com.oracle.database.jdbc" % "ojdbc11" % "23.7.0.25.01" % TestNote: The quick-start examples below use PostgreSQL. Add the corresponding driver dependency before running them.
docker run -d --name gatling-pg \
-e POSTGRES_USER=test -e POSTGRES_PASSWORD=test -e POSTGRES_DB=test \
-p 5432:5432 postgres:16import org.galaxio.gatling.jdbc.Predef._
import io.gatling.core.Predef._
class JdbcSimulation extends Simulation {
val dbConf = DB
.url("jdbc:postgresql://localhost:5432/test")
.username("test")
.password("test")
.maximumPoolSize(10)
val scn = scenario("JDBC Query")
.exec(
jdbc("select users")
.query("SELECT * FROM users WHERE id = 1")
.check(simpleCheck(_.nonEmpty))
)
setUp(scn.inject(atOnceUsers(1))).protocols(dbConf)
}import static org.galaxio.gatling.javaapi.JdbcDsl.*;
import static io.gatling.javaapi.core.CoreDsl.*;
public class JdbcSimulation extends Simulation {
var dbConf = DB()
.url("jdbc:postgresql://localhost:5432/test")
.username("test")
.password("test")
.maximumPoolSize(10)
.protocolBuilder();
var scn = scenario("JDBC Query")
.exec(
jdbc("select users")
.query("SELECT * FROM users WHERE id = 1")
.check(simpleCheck(simpleCheckType.NonEmpty))
);
{ setUp(scn.injectOpen(atOnceUsers(1)).protocols(dbConf)); }
}import org.galaxio.gatling.javaapi.JdbcDsl.*
import io.gatling.javaapi.core.CoreDsl.*
class JdbcSimulation : Simulation() {
val dbConf = DB()
.url("jdbc:postgresql://localhost:5432/test")
.username("test")
.password("test")
.maximumPoolSize(10)
.protocolBuilder()
val scn = scenario("JDBC Query")
.exec(
jdbc("select users")
.query("SELECT * FROM users WHERE id = 1")
.check(simpleCheck(simpleCheckType.NonEmpty))
)
init { setUp(scn.injectOpen(atOnceUsers(1)).protocols(dbConf)) }
}import org.galaxio.gatling.jdbc.Predef._
import scala.concurrent.duration._
val dataBase = DB
.url("jdbc:postgresql://localhost:5432/test")
.username("user")
.password("pass")
.maximumPoolSize(32)
.blockingPoolSize(32)
.queryTimeout(30.seconds)var dataBase = DB()
.url("jdbc:postgresql://localhost:5432/test")
.username("user")
.password("pass")
.maximumPoolSize(32)
.blockingPoolSize(32)
.queryTimeout(Duration.ofSeconds(30))
.protocolBuilder();val dataBase = DB()
.url("jdbc:postgresql://localhost:5432/test")
.username("user")
.password("pass")
.maximumPoolSize(32)
.blockingPoolSize(32)
.queryTimeout(Duration.ofSeconds(30))
.protocolBuilder()| Setting | Default | Description |
|---|---|---|
maximumPoolSize |
10 | Max connections in HikariCP pool |
minimumIdleConnections |
10 | Min idle connections |
blockingPoolSize |
= maximumPoolSize |
Fixed thread pool for blocking JDBC calls |
connectionTimeout |
1 minute | Connection acquisition timeout |
queryTimeout |
None | Statement query timeout (per query) |
JDBC calls are blocking, so the plugin runs them on a dedicated executor. blockingPoolSize defaults to maximumPoolSize to prevent unbounded native thread growth.
val hikariConfig = new HikariConfig()
hikariConfig.setJdbcUrl("jdbc:postgresql://localhost:5432/test")
hikariConfig.setMaximumPoolSize(16)
val dataBase = DB.hikariConfig(hikariConfig)jdbc("select users")
.query("SELECT * FROM users WHERE status = 'active'")
.check(simpleCheck(_.nonEmpty))Uses prepared statements. {param} placeholders are replaced with ? at execution time.
jdbc("find user")
.queryP("SELECT * FROM users WHERE id = {id} AND status = {status}")
.params("id" -> "#{userId}", "status" -> "active")jdbc("insert user")
.insertInto("users", Columns("id", "name", "email"))
.values("id" -> 1, "name" -> "#{userName}", "email" -> "#{email}")Java / Kotlin:
jdbc("insert user")
.insertInto("users", "id", "name", "email")
.values(Map.of("id", 1, "name", "#{userName}", "email", "#{email}"))Execute any SQL statement (DDL, DML, etc.) without result mapping:
jdbc("create table")
.rawSql("CREATE TABLE IF NOT EXISTS users (id INT PRIMARY KEY, name VARCHAR(100))")Java / Kotlin:
jdbc("create table")
.rawSql("CREATE TABLE IF NOT EXISTS users (id INT PRIMARY KEY, name VARCHAR(100))")Execute multiple insert/update operations in a single batch:
Scala:
jdbc("batch insert").batch(
insertInto("users", Columns("id", "name")).values("id" -> 1, "name" -> "Alice"),
insertInto("users", Columns("id", "name")).values("id" -> 2, "name" -> "Bob"),
)jdbc("batch update").batch(
update("users").set("name" -> "Updated").where("id = 1"),
update("users").set("name" -> "Updated2").where("id = 2"),
)Dynamic where values (since 1.5.0): bind session data as parameters rather than
interpolating it into the clause. A where(String) clause is author-fixed SQL and rejects
Gatling EL (#{…}) at scenario-build time; pass dynamic values through the parameterized
overload so they are bound as data and can never widen the predicate:
// Scala — {name} placeholders bound from the params
update("users").set("status" -> "BLOCKED").where("email = {email}", "email" -> "#{userEmail}")// Java / Kotlin
update("users").set(Map.of("status", "BLOCKED")).where("email = {email}", Map.of("email", "#{userEmail}"));Java / Kotlin:
jdbc("batch insert").batch(
insertInto("users", "id", "name").values(Map.of("id", 1, "name", "Alice")),
insertInto("users", "id", "name").values(Map.of("id", 2, "name", "Bob"))
)Execution order and grouping (since 1.3.0):
- Operations execute in the order you declare them — the plugin never reorders a batch.
- Operations with identical SQL text share one execution group (a single prepared statement) only when they are adjacent in the batch.
- Interleaving identical statements therefore produces more execution groups than grouping
them adjacently:
A, B, Aruns as three groups, whileA, A, Bruns as two. If you want fewer groups and your scenario allows it, place operations with the same SQL next to each other. - Grouping does not change transactional behavior: the whole batch still commits together or rolls back together.
Call stored procedures with IN and optional OUT parameters:
Scala:
jdbc("call procedure")
.call("my_procedure")
.params("inParam" -> "#{value}")With OUT parameters (values are stored in the Gatling session):
jdbc("call with out")
.call("my_procedure")
.params("inParam" -> "#{value}")
.outParams("outResult" -> java.sql.Types.INTEGER)Java / Kotlin:
jdbc("call procedure")
.call("my_procedure")
.params(Map.of("inParam", "#{value}"))
.outParams(Map.of("outResult", java.sql.Types.INTEGER))After execution, OUT parameter values are available in the session via #{outResult}.
| Check | Description |
|---|---|
allResults / allResults() |
Full result set as List[Map[String, Any]] |
simpleCheck(predicate) |
Custom boolean predicate over the result set |
simpleCheck(simpleCheckType.NonEmpty) |
Built-in Java/Kotlin check: result is non-empty |
simpleCheck(simpleCheckType.Empty) |
Built-in Java/Kotlin check: result is empty |
jdbc("select users")
.query("SELECT * FROM users")
.check(
simpleCheck(_.nonEmpty),
allResults.saveAs("rows"),
)jdbc("select users")
.query("SELECT * FROM users")
.check(
simpleCheck(simpleCheckType.NonEmpty),
allResults().saveAs("rows")
);Upgrading to 1.3.0 —
check(...)returns a new builder. Before 1.3.0 the Java query builder registered checks by mutating itself in place. Since 1.3.0check(...)returns a new builder and leaves the original unchanged (this fixed shared builder branches corrupting each other). Code that callscheck(...)and ignores the returned value no longer registers any checks — the load test keeps passing while asserting nothing.QueryActionBuilder builder = jdbc("select users").query("SELECT * FROM users"); // before 1.3.0 this registered the check; since 1.3.0 it registers nothing: builder.check(simpleCheck(simpleCheckType.NonEmpty)); // since 1.3.0 — use the returned builder: builder = builder.check(simpleCheck(simpleCheckType.NonEmpty)); // or chain the calls directly: jdbc("select users") .query("SELECT * FROM users") .check(simpleCheck(simpleCheckType.NonEmpty));The original builder instance stays valid and unchanged — reuse it to branch independent scenarios from a shared base.
The plugin supports Gatling Expression Language (EL) in SQL queries. Use #{variableName} to reference session values.
exec(session => session.set("tableName", "USERS"))
.exec(jdbc("dynamic query").query("SELECT * FROM #{tableName} WHERE id = #{id}")){param}in SQL — prepared statement placeholder (replaced with?)"#{var}"in.params()— Gatling EL, resolves from session at runtime
jdbc("parameterized query")
.queryP("SELECT * FROM users WHERE id = {id}")
.params("id" -> "#{userId}")jdbc("insert user")
.insertInto("users", "id", "name", "active")
.values(Map.of(
"id", 1,
"name", "#{userName}",
"active", true
));Release 1.5.0 hardens runtime correctness around SQL injection, secret handling, and NULL fidelity. Three changes are behavior changes — each fixes a silent-corruption or injection defect, so they ship as bug fixes in this minor release. Review the two migration items below if you use the affected features.
Previously, a feeder or map value of the four-character string "NULL" was silently written
as database NULL, making the literal text unstorable. It is now stored verbatim. Database
NULL comes only from a genuinely absent value (JVM null) or an explicit NullParam.
// Before 1.5.0: this wrote SQL NULL. From 1.5.0: it writes the text "NULL".
insertInto("t", Columns("name")).values("name" -> "NULL")
// To write SQL NULL, use JVM null (Java/Kotlin maps) or NullParam (Scala):
SQL("... {x}").withParams("x" -> NullParam)If a scenario relied on "NULL" meaning SQL NULL, switch it to NullParam / null. Data
that was previously NULLed will now be stored as text — that is the fix, not a regression.
A where("... #{value} ...") string now fails at scenario-build time, because interpolating
session data into the predicate is an injection vector. Replace it with the parameterized
overload (see Batch Operations):
// Before: where("email = '#{email}'") ← now rejected at build time
where("email = {email}", "email" -> "#{email}") // values bound as dataAuthor-fixed clauses without EL (where("status = 'ACTIVE'")) are unaffected. The previous
Expression[String] overload remains as a deprecated, documented escape hatch.
KO messages recorded into Gatling stats/reports are now rebuilt from structured fields
(ClassName [SQLState=…, code=…]) so they never carry feeder values (emails, passwords) into
shared artifacts. The full raw driver message is available on the plugin's DEBUG logger
(org.galaxio.gatling.jdbc.actions.ActionBase) — enable it to see raw detail while triaging.
Connection passwords and URL-embedded credentials are redacted from the protocol builder's
toString and from anything the plugin logs. Custom HikariCP data-source properties whose
names match password, secret, token, passphrase, credential, or apikey (case- and
separator-insensitive) are flagged with a build-time warning, because HikariCP prints custom
properties verbatim in its own DEBUG config dump — which the plugin cannot suppress.
Properties with names outside that set are not covered: name secret-bearing properties
recognizably, prefer setPassword (which HikariCP masks) over credentials in the JDBC URL,
and avoid DEBUG logging with real secrets in shared runs.
# Build
sbt compile
# Run unit tests
sbt test
# Run all tests (unit + integration; integration tests start PostgreSQL via Testcontainers)
sbt test
# Run Gatling example simulation (H2)
sbt "Gatling / testOnly org.galaxio.performance.jdbc.test.DebugTest"
# Check formatting
sbt scalafmtCheckAll
# Format code
sbt scalafmtAllApache License 2.0. See LICENSE for details.