Skip to main content

Stored Procedures & Functions

Define stored procedures and functions once with full type safety on both inputs and outputs. The builder tracks types statically — wrong argument types or missing parameters are compile errors, not runtime surprises.

Procedures

Void Procedures

Procedures with only IN parameters return Void (or Unit in Scala/Kotlin):

// A void procedure — no OUT parameters, just side effects
val auditLog =
DbProcedure.define("audit_log")
.input(PgTypes.text) // action
.input(PgTypes.text) // details
.build()

fun logAction(action: String, details: String) {
auditLog.call(action, details).transact(tx)
}

OUT Parameters

Add .out() to declare output parameters. The builder tracks their types:

// OUT parameters — the builder tracks output types statically
val getUser =
DbProcedure.define("get_user_by_id")
.input(PgTypes.int4) // user_id IN
.out(PgTypes.text) // name OUT
.out(PgTypes.text) // email OUT
.build()

// call() is fully typed — wrong argument types won't compile
fun findUser(userId: Int): Pair<String, String> =
getUser.call(userId).transact(tx)

The builder returns a typed definition that captures the input and output parameter types. The numbers in the type name encode the arity: Def1_2 means 1 input and 2 outputs. For functions, Def2 means 2 inputs (functions always have exactly one output — the return value). You never need to write these types explicitly — just use var (Java) or let type inference work (Kotlin/Scala).

INOUT Parameters

INOUT parameters count as both input and output. The value goes in and comes back modified:

// INOUT — the value goes in and comes back modified
val applyDiscount =
DbProcedure.define("apply_discount")
.input(PgTypes.text) // discount_code IN
.inout(PgTypes.numeric) // price INOUT — goes in, comes back modified
.build()

fun applyDiscount(code: String, price: BigDecimal): BigDecimal =
applyDiscount
.call(code, price)
.transact(tx)

Functions

Functions return a single value via SELECT instead of CALL:

// Functions use SELECT instead of CALL — every DbType reads correctly
val calcTax =
DbFunction.define("calculate_tax", PgTypes.numeric)
.input(PgTypes.numeric) // amount
.input(PgTypes.text) // region
.build()

// Zero-argument function
val nextId =
DbFunction.define("next_id", PgTypes.int4)
.build()

fun calculateTax(amount: BigDecimal, region: String): BigDecimal =
calcTax.call(amount, region)
.transact(tx)

Any Database

The procedure and function builders work with any database's types. Just use the right *Types class:

// Works with any database — just use the right types
val applyDiscount =
DbProcedure.define("apply_discount")
.input(OracleTypes.number) // amount IN
.inout(OracleTypes.varchar2) // status INOUT
.build()

val getBalance =
DbFunction.define("get_balance", OracleTypes.number)
.input(OracleTypes.varchar2) // account_id
.build()

How It Works

FeatureProcedures (DbProcedure)Functions (DbFunction)
SQL{call proc_name(?, ...)}SELECT func_name(?, ...)
StatementCallableStatementPreparedStatement
Inputs.input().input()
Outputs.out(), .inout()Return type (single value)
Max arity10 inputs, 10 outputs10 inputs
ResultVoid, single value, or TupleSingle value

Verifying Against the Database

Use Query Analysis to verify that your procedure and function definitions match what's actually in the database — parameter count, types, modes, and return types are all checked.

Database Support

OUT parameter support requires a DbOutParam implementation for each type. All standard types are supported for all six databases:

DatabaseProceduresFunctionsOUT/INOUT
PostgreSQLYesYesYes
MariaDB/MySQLYesYesYes
DuckDBN/AN/ANo (no stored procedure support)
OracleYesYesYes
SQL ServerYesYesYes
DB2YesYesYes