Skip to main content

Fragments

A Fragment is a composable SQL building block — it holds a SQL string together with its bound parameters. There are two ways to build fragments: string interpolation (Kotlin and Scala) and the builder pattern (all languages).

String Interpolation

Kotlin uses sql { } and Scala uses sql"" to build fragments from string templates. Database values are embedded as typed, bound parameters — never concatenated into the SQL string.

For a thorough explanation of how sql { } works internally and its thread safety guarantees, see Kotlin String Interpolation.

val query: Fragment =
sql { """
SELECT * FROM users
WHERE id = ${PgTypes.int4(userId)}
AND status = ${PgTypes.text("active")}
AND created_at > ${PgTypes.timestamptz(cutoffDate)}
""" }

// Execute safely — parameters are bound, not interpolated
val users: List<User> =
query.query(userCodec.all()).run(connection)

Inside the interpolation block, you can embed:

  • Bound values${PgTypes.int4(userId)} becomes a ? parameter
  • Other fragments${codec.columnList}, ${Fragment.whereAnd(filters)}, or another sql { } / sql"" block are spliced into the SQL

Builder Pattern

The builder pattern works in all languages and is useful for constructing fragments programmatically. Start from Fragment.of("SELECT …") (or Fragment.builder() for an empty start) and chain .value(type, x) / .append(fragment):

// Build fragments programmatically with the builder pattern
val frag: Fragment =
Fragment.of("SELECT * FROM users WHERE id = ")
.value(PgTypes.int4, 42)
.append(" AND active = ")
.value(PgTypes.bool, true)
.value() binds, .param() creates a hole

The vocabulary matters because both occupy the same SQL ? position:

  • .value(type, x)bound value, immediately captured into the Fragment. Produces a ready-to-execute Operation.
  • .param(type)parameter hole, filled later. Produces a Template — see Templates.

.param(type, value) does not exist — if you know the value, use .value(type, value).

Which style should I use?
  • Kotlin — Use sql { } for queries where all values are known. Use the builder pattern when you need parameter holes for Templates (Advanced).
  • Scala — Same guidance, using sql"" instead of sql { }.
  • Java — Use the builder pattern for everything (no string interpolation available).

Composing Fragments

Build small reusable fragments, then combine them into full queries. Static factories like Fragment.whereAnd(), Fragment.set(), and Fragment.comma() handle SQL syntax — commas, AND/OR separators, SET clauses — so you don't have to:

// Build small reusable filters
fun byName(name: String): Fragment =
sql { "name ILIKE ${PgTypes.text(name)}" }

fun cheaperThan(max: BigDecimal): Fragment =
sql { "price < ${PgTypes.numeric(max)}" }

// Compose dynamically — only include the filters that are present
fun query(): List<ProductRow> {
val filters = listOfNotNull(
byName("%widget%"),
maxPrice?.let(::cheaperThan)
)

return tx.transact { conn ->
sql { "SELECT * FROM product ${Fragment.whereAnd(filters)}" }
.query(rowCodec.all())
.run(conn)
}
}

The same approach works for UPDATE statements — build a list of assignments and let Fragment.set() join them:

// Dynamic UPDATE — only set the fields that changed
fun updateUser(userId: Int, newName: String?, newEmail: String?) {
val sets = listOfNotNull(
newName?.let { sql { "name = ${PgTypes.text(it)}" } },
newEmail?.let { sql { "email = ${PgTypes.text(it)}" } }
)

if (sets.isNotEmpty()) {
sql { "UPDATE users ${Fragment.set(sets)} WHERE id = ${PgTypes.int4(userId)}" }
.update()
.run(conn)
}
}

Other useful combinators: Fragment.and(), Fragment.or(), Fragment.whereOr(), Fragment.orderBy(), Fragment.comma(), Fragment.parentheses().

IN-clause helper

For IN clauses against dialects without native array types (MariaDB, SQL Server, Oracle, DB2), Fragment.valuesList(type, values) emits (?, ?, …) with each value bound as a typed parameter:

Fragment.of("SELECT * FROM emp WHERE id IN ")
.append(Fragment.valuesList(MariaTypes.int_, List.of(1, 2, 3)))
// SELECT * FROM emp WHERE id IN (?, ?, ?)

On PostgreSQL or DuckDB, prefer the native array idiom: .value(int4.array(), ids) with WHERE id = ANY(?).

valuesList throws IllegalArgumentException on an empty list — an empty IN() is SQL-invalid, so the caller has to branch (typically: return an empty result without issuing the query).