Composing Operations
Operations can be composed as values — combined, sequenced, and chained — so that multiple database actions run in a single transaction without manual connection handling.
Combining Independent Operations
.combineWith() combines two operations that don't depend on each other. Both run in the same transaction, and a function combines their results:
- Kotlin
- Java
- Scala
data class User(val id: Int, val name: String)
data class Order(val id: Int, val userId: Int, val product: String)
data class Dashboard(val userCount: Long, val recentOrders: List<Order>)
data class Stats(val userCount: Long, val orderCount: Long, val revenue: Long)
val orderCodec: RowCodec<Order> =
RowCodec.builder<Order>()
.field(PgTypes.int4, Order::id)
.field(PgTypes.int4, Order::userId)
.field(PgTypes.text, Order::product)
.build(::Order)
lateinit var tx: Transactor
// Combine two independent queries — both run in one transaction
val countUsers: Operation<Long> =
sql { "SELECT count(*) FROM users" }
.query(RowCodec.of(PgTypes.int8).exactlyOne())
val recentOrders: Operation<List<Order>> =
sql { "SELECT * FROM orders ORDER BY id DESC LIMIT 10" }
.query(orderCodec.all())
fun dashboard(): Dashboard =
countUsers
.combineWith(recentOrders, ::Dashboard)
.transact(tx)
// Three-way: all run in one transaction, results combined
val countOrders: Operation<Long> =
sql { "SELECT count(*) FROM orders" }
.query(RowCodec.of(PgTypes.int8).exactlyOne())
val totalRevenue: Operation<Long> =
sql { "SELECT coalesce(sum(amount), 0) FROM orders" }
.query(RowCodec.of(PgTypes.int8).exactlyOne())
fun stats(): Stats =
countUsers
.combineWith(countOrders, totalRevenue, ::Stats)
.transact(tx)
record User(int id, String name) {}
record Order(int id, int userId, String product) {}
record Dashboard(long userCount, List<Order> recentOrders) {}
record Stats(long userCount, long orderCount, long revenue) {}
static RowCodec<Order> orderCodec =
RowCodec.<Order>builder()
.field(PgTypes.int4, Order::id)
.field(PgTypes.int4, Order::userId)
.field(PgTypes.text, Order::product)
.build(Order::new);
Transactor tx = null; // placeholder
// Combine two independent queries — both run in one transaction
Operation<Long> countUsers =
Fragment.of("SELECT count(*) FROM users")
.query(RowCodec.of(PgTypes.int8).exactlyOne());
Operation<List<Order>> recentOrders =
Fragment.of("""
SELECT * FROM orders
ORDER BY id DESC LIMIT 10""")
.query(orderCodec.all());
Dashboard dashboard() {
return countUsers
.combineWith(recentOrders, Dashboard::new)
.transact(tx);
}
// Three-way: all run in one transaction, results combined
Operation<Long> countOrders =
Fragment.of("SELECT count(*) FROM orders")
.query(RowCodec.of(PgTypes.int8).exactlyOne());
Operation<Long> totalRevenue =
Fragment.of("""
SELECT coalesce(sum(amount), 0)
FROM orders""")
.query(RowCodec.of(PgTypes.int8).exactlyOne());
Stats stats() {
return countUsers
.combineWith(countOrders, totalRevenue, Stats::new)
.transact(tx);
}
case class User(id: Int, name: String)
case class Order(id: Int, userId: Int, product: String)
case class Dashboard(userCount: Long, recentOrders: List[Order])
case class Stats(userCount: Long, orderCount: Long, revenue: Long)
val orderCodec: RowCodec[Order] = RowCodec.builder[Order]()
.field(PgTypes.int4)(_.id)
.field(PgTypes.int4)(_.userId)
.field(PgTypes.text)(_.product)
.build(Order.apply)
var tx: Transactor = null // placeholder
// Combine two independent queries in one transaction
val countUsers: Operation[Long] =
sql"SELECT count(*) FROM users"
.query(RowCodec.of(PgTypes.int8).exactlyOne())
val recentOrders: Operation[List[Order]] =
sql"SELECT * FROM orders ORDER BY id DESC LIMIT 10"
.query(orderCodec.all())
def dashboard(): Dashboard =
countUsers.combineWith(recentOrders)(Dashboard.apply)
.transact(tx)
// Three-way: all run in one transaction
val countOrders: Operation[Long] =
sql"SELECT count(*) FROM orders"
.query(RowCodec.of(PgTypes.int8).exactlyOne())
val totalRevenue: Operation[Long] =
sql"SELECT coalesce(sum(amount), 0) FROM orders"
.query(RowCodec.of(PgTypes.int8).exactlyOne())
def stats(): Stats =
countUsers
.combineWith(countOrders, totalRevenue)(Stats.apply)
.transact(tx)
Running Multiple Writes
When you have several write operations and only care about completion (not individual results), use Operation.allOf():
- Kotlin
- Java
- Scala
// Run multiple writes in one transaction, discard individual results
val insertUser: Operation<Int> =
sql { "INSERT INTO users(name) VALUES(${PgTypes.text("Alice")})" }.update()
val insertAudit: Operation<Int> =
sql { "INSERT INTO audit_log(action) VALUES(${PgTypes.text("user_created")})" }.update()
val updateStats: Operation<Int> =
sql { "UPDATE stats SET user_count = user_count + 1" }.update()
fun createUserWithAudit() {
Operation.allOf(insertUser, insertAudit, updateStats).transact(tx)
}
// Run multiple writes in one transaction, discard individual results
Operation<Integer> insertUser =
Fragment.of("INSERT INTO users(name) VALUES(")
.value(PgTypes.text, "Alice")
.append(")")
.update();
Operation<Integer> insertAudit =
Fragment.of("""
INSERT INTO audit_log(action)
VALUES(\
""")
.value(PgTypes.text, "user_created")
.append(")")
.update();
Operation<Integer> updateStats =
Fragment.of("""
UPDATE stats
SET user_count = user_count + 1""")
.update();
void createUserWithAudit() {
Operation.allOf(
insertUser, insertAudit, updateStats
).transact(tx);
}
// Run multiple writes in one transaction
val insertUser: Operation[Int] =
sql"INSERT INTO users(name) VALUES(${PgTypes.text("Alice")})"
.update()
val insertAudit: Operation[Int] =
sql"INSERT INTO audit_log(action) VALUES(${PgTypes.text("user_created")})"
.update()
val updateStats: Operation[Int] =
sql"UPDATE stats SET user_count = user_count + 1"
.update()
def createUserWithAudit(): Unit =
Operation.allOf(
insertUser, insertAudit, updateStats
).transact(tx)
Sequencing a List
When you have a dynamic list of operations, Operation.sequence() runs them all and collects the results:
- Kotlin
- Java
- Scala
// Execute a list of operations and collect all results
val names = listOf("Alice", "Bob", "Charlie")
fun insertAll(): List<Int> {
val inserts: List<Operation<Int>> = names.map { name ->
sql { "INSERT INTO users(name) VALUES(${PgTypes.text(name)}) RETURNING id" }
.query(RowCodec.of(PgTypes.int4).exactlyOne())
}
return Operation.sequence(inserts).transact(tx)
}
// Execute a list of operations and collect all results
List<String> names = List.of("Alice", "Bob", "Charlie");
List<Integer> insertAll() {
List<Operation<Integer>> inserts =
names.stream()
.<Operation<Integer>>map(name ->
Fragment.of("""
INSERT INTO users(name)
VALUES(\
""")
.value(PgTypes.text, name)
.append(") RETURNING id")
.query(RowCodec.of(PgTypes.int4)
.exactlyOne()))
.toList();
return Operation.sequence(inserts).transact(tx);
}
val names: List[String] =
List("Alice", "Bob", "Charlie")
def insertAll(): List[Int] =
val inserts: List[Operation[Int]] =
names.map { name =>
sql"""INSERT INTO users(name)
VALUES(${PgTypes.text(name)})
RETURNING id"""
.query(RowCodec.of(PgTypes.int4).exactlyOne())
}
Operation.sequence(inserts).transact(tx)
Data Flow Between Operations
Use .then() to feed one operation's result into the next operation's template. The first operation runs, and its result becomes the input to the template:
- Kotlin
- Java
- Scala
// Define templates
val insertUser: Template<String, Int> =
sql { "INSERT INTO users(name) VALUES(" }
.param(PgTypes.text)
.append(") RETURNING id")
.query(RowCodec.of(PgTypes.int4).exactlyOne())
val ordersByUser: Template<Int, List<Order>> =
sql { "SELECT id, user_id, product FROM orders WHERE user_id = " }
.param(PgTypes.int4)
.query(orderCodec.all())
// Chain: insert user, then use returned id to fetch their orders
fun insertAndFetchOrders(): List<Order> =
insertUser.on("Alice")
.then(ordersByUser)
.transact(tx)
// Define templates
Template<String, Integer> insertUser =
Fragment.of("INSERT INTO users(name) VALUES(")
.param(PgTypes.text)
.append(") RETURNING id")
.query(RowCodec.of(PgTypes.int4).exactlyOne());
Template<Integer, List<Order>> ordersByUser =
Fragment.of("""
SELECT id, user_id, product
FROM orders WHERE user_id =
""")
.param(PgTypes.int4)
.query(orderCodec.all());
// Chain: insert user, then use returned id to fetch their orders
List<Order> insertAndFetchOrders() {
return insertUser.on("Alice")
.then(ordersByUser)
.transact(tx);
}
// Define templates
val insertUser: Template[String, Int] =
sql"INSERT INTO users(name) VALUES("
.param(PgTypes.text)
.append(") RETURNING id")
.query(RowCodec.of(PgTypes.int4).exactlyOne())
val ordersByUser: Template[Int, List[Order]] =
sql"SELECT id, user_id, product FROM orders WHERE user_id = "
.param(PgTypes.int4)
.query(orderCodec.all())
// Chain: insert user, then fetch their orders
def insertAndFetchOrders(): List[Order] =
insertUser.on("Alice")
.andThen(ordersByUser)
.transact(tx)
When the first operation returns a record and the template uses .from(), use .then() with the Template.From directly:
- Kotlin
- Java
- Scala
// 1-param template: insert user, return id and name
val insertUser: Template<String, NewUser> =
sql { "INSERT INTO users(name) VALUES(" }
.param(PgTypes.text)
.append(") RETURNING id, name")
.query(newUserCodec.exactlyOne())
// 2-param template: log the creation with both fields
val logCreation: Template.Update2<Int, String> =
sql { "INSERT INTO audit_log(user_id, username) VALUES(" }
.param(PgTypes.int4)
.append(", ")
.param(PgTypes.text)
.append(")")
.update()
// Chain: .from() adapts the 2-param template to accept NewUser
fun insertAndLog(): Int =
insertUser.on("Alice")
.then(logCreation.from(NewUser::id, NewUser::name))
.transact(tx)
// 1-param template: insert user, return id and name
Template<String, NewUser> insertUser =
Fragment.of("INSERT INTO users(name) VALUES(")
.param(PgTypes.text)
.append(") RETURNING id, name")
.query(newUserCodec.exactlyOne());
// 2-param template: log the creation with both fields
Template.Update2<Integer, String> logCreation =
Fragment.of("INSERT INTO audit_log(user_id, username) VALUES(")
.param(PgTypes.int4)
.append(", ")
.param(PgTypes.text)
.append(")")
.update();
// Chain: .from() adapts the 2-param template to accept NewUser
int insertAndLog() {
return insertUser.on("Alice")
.then(logCreation.from(NewUser::id, NewUser::name))
.transact(tx);
}
// 1-param template: insert user, return id and name
val insertUser: Template[String, NewUser] =
sql"INSERT INTO users(name) VALUES("
.param(PgTypes.text)
.append(") RETURNING id, name")
.query(newUserCodec.exactlyOne())
// 2-param template: log the creation with both fields
val logCreation: Template.Update2[Int, String] =
sql"INSERT INTO audit_log(user_id, username) VALUES("
.param(PgTypes.int4)
.append(", ")
.param(PgTypes.text)
.append(")")
.update()
// Chain: .from() adapts the 2-param template to accept NewUser
def insertAndLog(): Int =
insertUser.on("Alice")
.andThen(logCreation.from(_.id, _.name))
.transact(tx)
Conditional Execution
Operation.ifEmpty() implements the find-or-create pattern: run the first operation, and if it returns empty (empty Optional, null, or None), run the fallback instead:
- Kotlin
- Java
- Scala
// Find-or-create pattern
val findUser: Template<String, User?> =
sql { "SELECT id, name, email FROM users WHERE email = " }
.param(PgTypes.text)
.query(userCodec.maxOne())
val createUser: Template.Query2<String, String, User> =
sql { "INSERT INTO users(name, email) VALUES(" }
.param(PgTypes.text)
.append(", ")
.param(PgTypes.text)
.append(") RETURNING *")
.query(userCodec.exactlyOne())
fun findOrCreate(): User =
Operation.ifEmpty(
findUser.on(email),
createUser.on(name, email)
).transact(tx)
// Find-or-create pattern
Template<String, Optional<User>> findUser =
Fragment.of("""
SELECT id, name, email
FROM users WHERE email =
""")
.param(PgTypes.text)
.query(userCodec.maxOne());
Template.Query2<String, String, User> createUser =
Fragment.of("""
INSERT INTO users(name, email)
VALUES(\
""")
.param(PgTypes.text).append(", ")
.param(PgTypes.text)
.append(") RETURNING *")
.query(userCodec.exactlyOne());
User findOrCreate() {
return Operation.ifEmpty(
findUser.on(email),
createUser.on(name, email)
).transact(tx);
}
// Find-or-create pattern
val findUser: Template[String, Option[User]] =
sql"SELECT id, name, email FROM users WHERE email = "
.param(PgTypes.text)
.query(userCodec.maxOne())
val createUser: Template.Query2[String, String, User] =
sql"INSERT INTO users(name, email) VALUES("
.param(PgTypes.text).append(", ")
.param(PgTypes.text)
.append(") RETURNING *")
.query(userCodec.exactlyOne())
def findOrCreate(): User =
Operation.ifEmpty(
findUser.on(email),
createUser.on(name, email)
).transact(tx)
Analyzing Composed Operations
QueryAnalyzer can walk the entire operation tree and analyze every SQL statement in one call. See Query Analysis for details.
- Kotlin
- Java
- Scala
fun analyzeComposedOperation() {
// Build a composed operation
val transaction: Operation<*> =
insertUser.on("Alice")
.thenIgnore(allUsers)
// Analyze every SQL statement in the tree — one call
val results: List<QueryAnalysis> =
QueryAnalyzer.analyze(transaction, conn)
for (analysis in results) {
if (!analysis.succeeded()) {
System.err.println(analysis.report())
}
}
}
void analyzeComposedOperation() {
// Build a composed operation
Operation<?> transaction =
insertUser.on("Alice").thenIgnore(allUsers);
// Analyze every SQL statement in the tree
List<QueryAnalysis> results =
QueryAnalyzer.analyze(transaction, conn);
for (var analysis : results) {
if (!analysis.succeeded()) {
System.err.println(analysis.report());
}
}
}
def analyzeComposedOperation(): Unit =
// Build a composed operation
val transaction: Operation[?] =
insertUser.on("Alice").thenIgnore(allUsers)
// Analyze every statement in the tree
val results: List[QueryAnalysis] =
QueryAnalyzer.analyze(transaction, conn)
for analysis <- results do
if !analysis.succeeded() then
System.err.println(analysis.report())