Skip to main content

Fragments

A Fragment is a SQL string paired 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 the way to compose 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 that 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).