Skip to main content

Fragments

A Fragment is a SQL building block that holds a SQL string together with its bound parameters. Build fragments with string interpolation (Kotlin and Scala) or the builder pattern (all languages).

String interpolation

Kotlin uses sql { } and Scala uses sql"" to build fragments from string templates. Database values become typed, bound parameters. They are 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)
Which style should I use?
  • Kotlin — Use sql { } for queries where all values are known. Use the builder pattern when you need to compose fragments programmatically.
  • Scala — Same guidance, using sql"" instead of sql { }.
  • Java — Use the builder pattern for everything (no string interpolation available).

Composing fragments

For dynamic queries — searches with optional filters, conditional clauses — chain .optionally(value).append(sql, type) onto a base fragment. Each optionally is a branch point Query Analysis expands into all 2N possible SQL shapes, so every code path is verified against the schema:

// Compose dynamic filters with the `optionally` DSL. Each `.optionally().append(...)`
// is a branch point Query Analysis verifies against the schema, even when
// the runtime never takes that branch at this call site.
fun query(): List<ProductRow> = tx.execute(
sql { "SELECT id, name, price FROM product WHERE 1 = 1" }
.optionally(namePattern).append(" AND name ILIKE ", PgTypes.text)
.optionally(maxPrice) .append(" AND price < ", PgTypes.numeric)
.query(rowCodec.all())
)

For list-shaped composition (joining a List<Fragment> from arbitrary sources), static factories like Fragment.whereAnd(), Fragment.set(), and Fragment.comma() handle SQL syntax (commas, AND/OR separators, SET clauses):

// 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().

See Dynamic Queries for a comparison of the two styles, including trade-offs around Query Analysis coverage.

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).