Templates
Most parameterized queries work fine as plain functions that build a Fragment with bound values. Upgrade to Templates when you need: batch execution with .onMany(), dynamic query variants with .optionally(), or reusable query values for Query Analysis.
Templates let you define the SQL structure once and supply values later. Use .param(type) (without a value) to create a typed parameter hole. This produces a Template — a reusable operation factory that can be analyzed by Query Analysis without providing concrete values:
- Kotlin
- Java
- Scala
// Define a reusable template — SQL structure is fixed, values come later
val findByEmail: Template<String, User?> =
Fragment.of("SELECT id, name, email FROM users WHERE email = ")
.param(PgTypes.text)
.query(userCodec.maxOne())
// Fill the template with a value to get a concrete operation
fun findAlice(): User? =
findByEmail.on("alice@example.com").transact(tx)
// Reuse the same template with different values
fun findBob(): User? =
findByEmail.on("bob@example.com").transact(tx)
// Define a reusable template — SQL structure is fixed, values come later
Template<String, Optional<User>> findByEmail =
Fragment.of("""
SELECT id, name, email
FROM users WHERE email =
""")
.param(PgTypes.text)
.query(userCodec.maxOne());
// Fill the template with a value to get a concrete operation
Optional<User> findAlice() {
return findByEmail.on("alice@example.com").transact(tx);
}
// Reuse the same template with different values
Optional<User> findBob() {
return findByEmail.on("bob@example.com").transact(tx);
}
// Reusable template - SQL is fixed, values come later.
// sql"..." cannot be used with unbound parameters;
// the builder API provides type-safe Template construction.
val findByEmail: Template[String, Option[User]] =
Fragment.of(
"SELECT id, name, email FROM users WHERE email = "
).param(PgTypes.text)
.query(userCodec.maxOne())
// Fill the template to get a concrete operation
def findAlice(): Option[User] =
findByEmail.on("alice@example.com").transact(tx)
// Reuse with different values
def findBob(): Option[User] =
findByEmail.on("bob@example.com").transact(tx)
Fill the template with .on(value) to get a concrete operation.
Use bound values (.value(type, value), sql { }, sql"") when all values are known at definition time — they produce a Fragment directly.
Use Templates (.param(type)) when values come later — the SQL structure is fixed but values are supplied per-call. Templates also enable batch operations and dynamic queries with compile-time analysis of all variants.
Multiple Parameters
You can mix .value(type, value) (bound immediately) with .param(type) (filled later) in the same fragment. Only the unbound parameters become template parameters:
- Kotlin
- Java
- Scala
// Mix bound and unbound parameters in the same template.
// Status is fixed at "active"; name filter and limit vary per call.
val activeUsersByName: Template.Query2<String, Int, List<User>> =
sql { "SELECT id, name, status FROM users WHERE status = " }
.value(PgTypes.text, "active")
.append(" AND name ILIKE ")
.param(PgTypes.text)
.append(" ORDER BY name LIMIT ")
.param(PgTypes.int4)
.query(userCodec.all())
fun example(): List<User> =
activeUsersByName.on("%alice%", 10).transact(tx)
// Mix bound and unbound parameters in the same template.
// Status is fixed at "active"; name filter and limit vary per call.
Template.Query2<String, Integer, List<User>> activeUsersByName =
Fragment.of("""
SELECT id, name, status
FROM users WHERE status =
""")
.value(PgTypes.text, "active")
.append(" AND name ILIKE ")
.param(PgTypes.text)
.append(" ORDER BY name LIMIT ")
.param(PgTypes.int4)
.query(userCodec.all());
List<User> example() {
return activeUsersByName.on("%alice%", 10).transact(tx);
}
// Mix bound and unbound parameters in the same template.
// Status is fixed at "active"; name filter and limit vary per call.
val activeUsersByName: Template.Query2[String, Int, List[User]] =
sql"SELECT id, name, status FROM users WHERE status = "
.value(PgTypes.text, "active")
.append(" AND name ILIKE ")
.param(PgTypes.text)
.append(" ORDER BY name LIMIT ")
.param(PgTypes.int4)
.query(userCodec.all())
def example(): List[User] =
activeUsersByName.on("%alice%", 10).transact(tx)
Packaging Parameters in a Record
When a template has multiple parameters, use .from() to map a record's fields to the template parameters. This gives each parameter a name and lets callers pass a single object:
- Kotlin
- Java
- Scala
// A data class gives names to each template parameter
data class InsertUser(val name: String, val email: String)
// .from() maps data class fields to template params
val insertUser: Template.From<InsertUser, Int> =
sql { "INSERT INTO users(name, email) VALUES(" }
.param(PgTypes.text)
.append(", ")
.param(PgTypes.text)
.append(")")
.update()
.from(InsertUser::name, InsertUser::email)
// Callers pass the data class
fun createUser(): Int =
insertUser.on(InsertUser("Alice", "alice@example.com"))
.transact(tx)
// A record gives names to each template parameter
record InsertUser(String name, String email) {}
// .from() maps record fields to template params
Template.From<InsertUser, Integer> insertUser =
Fragment.of("INSERT INTO users(name, email) VALUES(")
.param(PgTypes.text)
.append(", ")
.param(PgTypes.text)
.append(")")
.update()
.from(InsertUser::name, InsertUser::email);
// Callers pass the record
int createUser() {
return insertUser.on(new InsertUser("Alice", "alice@example.com"))
.transact(tx);
}
// A case class gives names to each template parameter
case class InsertUser(name: String, email: String)
// .from() maps case class fields to template params
val insertUser: Template.From[InsertUser, Int] =
sql"INSERT INTO users(name, email) VALUES("
.param(PgTypes.text)
.append(", ")
.param(PgTypes.text)
.append(")")
.update()
.from(_.name, _.email)
// Callers pass the case class
def createUser(): Int =
insertUser.on(InsertUser("Alice", "alice@example.com"))
.transact(tx)
Template.From implements Template, so it works with all combinators including .then() for chaining operations and .optionally() for dynamic queries.
Batch Operations
Use a template with .onMany() to batch-insert or batch-update rows. The template defines the SQL once, and .onMany() executes it for each row using JDBC batch mode (addBatch() / executeBatch()):
- Kotlin
- Java
- Scala
// Batch insert — all columns as parameters
val insertAll: RowTemplate.Update<Product> =
Fragment.insertInto("product", productCodec)
fun insertProducts(products: List<Product>): IntArray =
insertAll.onMany(products.iterator()).run(conn)
// Batch insert — skip auto-generated ID column
val insertAutoId: RowTemplate.Update<Product> =
Fragment.insertInto("product", productCodec, "id")
fun insertProductsAutoId(products: List<Product>): IntArray =
insertAutoId.onMany(products.iterator()).run(conn)
// Batch insert — all columns as parameters
RowTemplate.Update<Product> insertAll =
Fragment.insertInto("product", productCodec);
int[] insertProducts(List<Product> products) {
return insertAll.onMany(products.iterator()).run(conn);
}
// Batch insert — skip auto-generated ID column
RowTemplate.Update<Product> insertAutoId =
Fragment.insertInto("product", productCodec, "id");
int[] insertProductsAutoId(List<Product> products) {
return insertAutoId.onMany(products.iterator()).run(conn);
}
// Batch insert — all columns as parameters
val insertAll: RowTemplate.Update[Product] =
Fragment.insertInto("product", productCodec)
def insertProducts(products: List[Product]): Array[Int] =
insertAll.onMany(products.iterator).run(conn)
// Batch insert — skip auto-generated ID column
val insertAutoId: RowTemplate.Update[Product] =
Fragment.insertInto("product", productCodec, "id")
def insertProductsAutoId(products: List[Product]): Array[Int] =
insertAutoId.onMany(products.iterator).run(conn)
Driver-level optimizations like .reWriteBatchedInserts() (PostgreSQL), .useBulkStmts() (MariaDB), and .useBulkCopyForBatchInsert() (SQL Server) must be enabled on the connection config for best performance.
For PostgreSQL high-throughput inserts, use streaming inserts with the COPY protocol instead.
Dynamic Templates
.optionally() wraps a fragment so it is included in the SQL when a value is present and omitted entirely when absent. Each combination of present/absent filters produces a structurally different query, and Query Analysis verifies all of them automatically.
Optional Filters
Use .optionally() with a parameterized fragment to create a filter that is included when a value is provided and skipped when absent:
- Kotlin
- Java
- Scala
// A search with an optional name filter.
// When present, the filter is applied; when absent, it's skipped.
val searchUsers: Template<String?, List<User>> =
sql { "SELECT id, name, email FROM users WHERE 1=1" }
.optionally(
sql { " AND name ILIKE " }.param(PgTypes.text))
.query(userCodec.all())
// Apply filter
fun filtered(): List<User> =
searchUsers.on("%alice%").transact(tx)
// Skip filter — returns all users
fun all(): List<User> =
searchUsers.on(null).transact(tx)
// A search with an optional name filter.
// When present, the filter is applied; when absent, it's skipped.
Template<Optional<String>, List<User>> searchUsers =
Fragment.of("""
SELECT id, name, email
FROM users WHERE 1=1
""")
.optionally(
Fragment.of(" AND name ILIKE ").param(PgTypes.text))
.query(userCodec.all());
// Apply filter
List<User> filtered() {
return searchUsers.on(Optional.of("%alice%")).transact(tx);
}
// Skip filter — returns all users
List<User> all() {
return searchUsers.on(Optional.empty()).transact(tx);
}
// A search with an optional name filter.
// When present, the filter is applied; when absent, it's skipped.
val searchUsers: Template[Option[String], List[User]] =
sql"SELECT id, name, email FROM users WHERE 1=1"
.optionally(
sql" AND name ILIKE ".param(PgTypes.text))
.query(userCodec.all())
// Apply filter
def filtered(): List[User] =
searchUsers.on(Some("%alice%")).transact(tx)
// Skip filter - returns all users
def all(): List[User] =
searchUsers.on(None).transact(tx)
The template parameter type reflects the optionality — Optional<String> in Java, String? in Kotlin, Option[String] in Scala.
WHERE 1=1The WHERE 1=1 pattern lets you unconditionally prefix every filter with AND — since 1=1 is always true, the query works correctly whether zero, one, or all filters are present. This avoids conditional logic to decide whether to emit WHERE or AND.
For non-template scenarios, Fragment.whereAnd(filters) handles this automatically.
Boolean Flags
For SQL chunks without parameters (e.g., AND active = TRUE), pass a plain Fragment to .optionally(). The template parameter becomes a Boolean — true includes the chunk, false skips it:
- Kotlin
- Java
- Scala
val activeUsers: Template<Boolean, List<User>> =
sql { "SELECT id, name, email FROM users WHERE 1=1" }
.optionally(sql { " AND active = TRUE" })
.query(userCodec.all())
// Include the filter
fun onlyActive(): List<User> =
activeUsers.on(true).transact(tx)
// Skip the filter — returns all users
fun all(): List<User> =
activeUsers.on(false).transact(tx)
Template<Boolean, List<User>> activeUsers =
Fragment.of("""
SELECT id, name, email
FROM users WHERE 1=1
""")
.optionally(Fragment.of(" AND active = TRUE"))
.query(userCodec.all());
// Include the filter
List<User> onlyActive() {
return activeUsers.on(true).transact(tx);
}
// Skip the filter — returns all users
List<User> all() {
return activeUsers.on(false).transact(tx);
}
val activeUsers: Template[Boolean, List[User]] =
sql"SELECT id, name, email FROM users WHERE 1=1"
.optionally(sql" AND active = TRUE")
.query(userCodec.all())
// Include the filter
def onlyActive(): List[User] =
activeUsers.on(true).transact(tx)
// Skip the filter - returns all users
def all(): List[User] =
activeUsers.on(false).transact(tx)
Multiple Optional Filters
Chain multiple .optionally() calls to build queries with many independent filters. Each adds a template parameter:
- Kotlin
- Java
- Scala
// Multiple optional filters — each independently present or absent
val search: Template.Query3<String?, String?, Boolean, List<User>> =
sql { "SELECT id, name, email FROM users WHERE 1=1" }
.optionally(
sql { " AND name ILIKE " }.param(PgTypes.text))
.optionally(
sql { " AND email ILIKE " }.param(PgTypes.text))
.optionally(
sql { " AND active = TRUE" })
.append(" ORDER BY name")
.query(userCodec.all())
// Each combination is type-safe
fun example(): List<User> =
search.on("%alice%", null, true).transact(tx)
// Query analysis expands all 2³ = 8 combinations automatically
fun verifyAllVariants() {
checker.check(search)
}
// Multiple optional filters — each independently present or absent
Template.Query3<Optional<String>, Optional<String>, Boolean, List<User>>
search = Fragment.of("""
SELECT id, name, email FROM users WHERE 1=1
""")
.optionally(
Fragment.of(" AND name ILIKE ").param(PgTypes.text))
.optionally(
Fragment.of(" AND email ILIKE ").param(PgTypes.text))
.optionally(
Fragment.of(" AND active = TRUE"))
.append(" ORDER BY name")
.query(userCodec.all());
// Each combination is type-safe
List<User> example() {
return search
.on(Optional.of("%alice%"), Optional.empty(), true)
.transact(tx);
}
// Query analysis expands all 2³ = 8 combinations automatically
void verifyAllVariants() {
checker.check(search);
}
// Multiple optional filters - each independently present or absent
val search: Template.Query3[Option[String], Option[String], Boolean, List[User]] =
sql"SELECT id, name, email FROM users WHERE 1=1"
.optionally(
sql" AND name ILIKE ".param(PgTypes.text))
.optionally(
sql" AND email ILIKE ".param(PgTypes.text))
.optionally(
sql" AND active = TRUE")
.append(" ORDER BY name")
.query(userCodec.all())
// Each combination is type-safe
def example(): List[User] =
search.on(Some("%alice%"), None, true).transact(tx)
// Query analysis expands all 2^3 = 8 combinations automatically
def verifyAllVariants(): Unit =
checker.check(search)
With 3 optional filters, there are 2³ = 8 possible query structures. For example, calling the template above with two different inputs:
search.on("alice", null, true)
→ SELECT id, name, email FROM users WHERE 1=1 AND name ILIKE ? AND active = TRUE ORDER BY name
search.on(null, null, false)
→ SELECT id, name, email FROM users WHERE 1=1 ORDER BY name
checker.check(search) expands and verifies all 8 combinations against the database — you don't test them individually.
Grouped Parameters
When an optional clause needs multiple parameters (e.g., a BETWEEN range), pass a multi-parameter builder. The grouped parameters are provided or omitted together as a single unit:
- Kotlin
- Java
- Scala
// When an optional clause needs multiple parameters,
// pass a multi-parameter builder.
// The grouped parameters are provided or omitted together.
val byPriceRange: Template<Pair<BigDecimal, BigDecimal>?, List<Product>> =
sql { "SELECT id, name, price FROM products WHERE 1=1" }
.optionally(
sql { " AND price BETWEEN " }
.param(PgTypes.numeric)
.append(" AND ")
.param(PgTypes.numeric))
.query(productCodec.all())
// With range
fun inRange(): List<Product> =
byPriceRange
.on(BigDecimal("10") to BigDecimal("50"))
.transact(tx)
// Without range — returns all products
fun all(): List<Product> =
byPriceRange.on(null).transact(tx)
// When an optional clause needs multiple parameters,
// pass a multi-parameter builder.
// The grouped parameters are provided or omitted together.
Template<Optional<Tuple.Tuple2<BigDecimal, BigDecimal>>, List<Product>>
byPriceRange = Fragment.of("""
SELECT id, name, price FROM products WHERE 1=1
""")
.optionally(
Fragment.of(" AND price BETWEEN ")
.param(PgTypes.numeric)
.append(" AND ")
.param(PgTypes.numeric))
.query(productCodec.all());
// With range
List<Product> inRange() {
return byPriceRange
.on(Optional.of(Tuple.of(
new BigDecimal("10"), new BigDecimal("50"))))
.transact(tx);
}
// Without range — returns all products
List<Product> all() {
return byPriceRange.on(Optional.empty()).transact(tx);
}
// When an optional clause needs multiple parameters,
// pass a multi-parameter builder.
// The grouped parameters are provided or omitted together.
val byPriceRange: Template[Option[(BigDecimal, BigDecimal)], List[Product]] =
sql"SELECT id, name, price FROM products WHERE 1=1"
.optionally(
sql" AND price BETWEEN "
.param(PgTypes.numeric)
.append(" AND ")
.param(PgTypes.numeric))
.query(productCodec.all())
// With range
def inRange(): List[Product] =
byPriceRange
.on(Some((BigDecimal("10"), BigDecimal("50"))))
.transact(tx)
// Without range - returns all products
def all(): List[Product] =
byPriceRange.on(None).transact(tx)
Packaging Filters in a Record
As the number of optional predicates grows, the raw template signature becomes unwieldy. Use .from() with getter references to map a record (or data class / case class) directly to template parameters — callers just pass the record:
- Kotlin
- Java
- Scala
// Package filters into a data class so callers see a clean API
data class UserSearch(
val name: String?,
val email: String?,
val activeOnly: Boolean
)
// .from() maps getters to template params
private val searchTemplate: Template.From<UserSearch, List<User>> =
sql { "SELECT id, name, email FROM users WHERE 1=1" }
.optionally(
sql { " AND name ILIKE " }.param(PgTypes.text))
.optionally(
sql { " AND email ILIKE " }.param(PgTypes.text))
.optionally(
sql { " AND active = TRUE" })
.append(" ORDER BY name")
.query(userCodec.all())
.from(UserSearch::name, UserSearch::email,
UserSearch::activeOnly)
// Callers just pass the data class
fun searchUsers(search: UserSearch): List<User> =
searchTemplate.on(search).transact(tx)
fun example(): List<User> {
val search = UserSearch(
name = "%alice%", email = null, activeOnly = true)
return searchUsers(search)
}
// Package filters into a record so callers see a clean API
record UserSearch(
Optional<String> name,
Optional<String> email,
boolean activeOnly
) {}
// .from() maps getters to template params
private static final Template.From<UserSearch, List<User>>
searchTemplate = Fragment.of("""
SELECT id, name, email FROM users WHERE 1=1
""")
.optionally(
Fragment.of(" AND name ILIKE ").param(PgTypes.text))
.optionally(
Fragment.of(" AND email ILIKE ").param(PgTypes.text))
.optionally(
Fragment.of(" AND active = TRUE"))
.append(" ORDER BY name")
.query(userCodec.all())
.from(UserSearch::name, UserSearch::email,
UserSearch::activeOnly);
// Callers just pass the record
List<User> searchUsers(UserSearch search) {
return searchTemplate.on(search).transact(tx);
}
List<User> example() {
var search = new UserSearch(
Optional.of("%alice%"), Optional.empty(), true);
return searchUsers(search);
}
// Package filters into a case class so callers see a clean API
case class UserSearch(
name: Option[String],
email: Option[String],
activeOnly: Boolean
)
// .from() maps getters to template params
private val searchTemplate: Template.From[UserSearch, List[User]] =
sql"SELECT id, name, email FROM users WHERE 1=1"
.optionally(
sql" AND name ILIKE ".param(PgTypes.text))
.optionally(
sql" AND email ILIKE ".param(PgTypes.text))
.optionally(
sql" AND active = TRUE")
.append(" ORDER BY name")
.query(userCodec.all())
.from(_.name, _.email, _.activeOnly)
// Callers just pass the case class
def searchUsers(search: UserSearch): List[User] =
searchTemplate.on(search).transact(tx)
def example(): List[User] =
val search = UserSearch(
name = Some("%alice%"), email = None, activeOnly = true)
searchUsers(search)