Exposed provides basic support for classic SQL functions. This topic consists of definitions for those functions, and their usage examples. It also explains how to define custom functions.
For the function examples below, consider the following table:
package org.example.tables
import org.jetbrains.exposed.v1.core.Table
const val MAX_TITLE_LENGTH = 150
const val MAX_REGION_LENGTH = 50
const val REVENUE_PRECISION = 12
const val REVENUE_SCALE = 2
object FilmBoxOfficeTable : Table() {
val title = varchar("title", MAX_TITLE_LENGTH)
val region = varchar("region", MAX_REGION_LENGTH)
val revenue = decimal("revenue", REVENUE_PRECISION, REVENUE_SCALE)
val month = integer("month")
val year = integer("year")
}
How to use functions
To retrieve the result of an SQL function result from a query using .select(), declare the function as a variable first:
val lowerCaseTitle = FilmBoxOfficeTable.title.lowerCase()
val upperCaseRegion = FilmBoxOfficeTable.region.upperCase().alias("reg_all_caps")
SQL functions can be chained and combined as needed. The example below generates SQL that concatenates the string values stored in two columns, before wrapping the function in TRIM() and LOWER():
To convert a string expression to lower-case or upper-case, use the .lowerCase() and .upperCase() functions respectively.
val lowerCaseTitle = FilmBoxOfficeTable.title.lowerCase()
Substring
The .substring() function returns a substring value from the specified start and with the specified length.
val shortenedTitle = FilmBoxOfficeTable.title.substring(start = 1, length = 3)
Concatenate
The concat() function returns a string value that concatenates the text representations of all non-null input values, separated by an optional separator.
The .locate() function returns the index of the first occurrence of a specified substring, or 0 if the substring is not found.
val firstXSIndex = FilmBoxOfficeTable.title.locate("XS")
Character length
The .charLength() function returns the length, measured in characters, or null if the String value is null.
val titleLength = FilmBoxOfficeTable.title.charLength()
Aggregate functions
These functions should most likely be used in queries with .groupBy().
Min/Max/Average
To get the minimum, maximum, and average values, use the .min().max() and .avg() functions respectively. These functions can be applied to any comparable expression:
val minRevenue = FilmBoxOfficeTable.revenue.min()
val maxRevenue = FilmBoxOfficeTable.revenue.max()
val averageRevenue = FilmBoxOfficeTable.revenue.avg()
val revenueStats = FilmBoxOfficeTable
.select(minRevenue, maxRevenue, averageRevenue, FilmBoxOfficeTable.region)
.groupBy(FilmBoxOfficeTable.region)
.map {
Triple(it[minRevenue], it[maxRevenue], it[averageRevenue])
}
Sum/Count
You can use SQL functions like SUM() and COUNT() directly with a column expression:
val revenueSum = FilmBoxOfficeTable.revenue.sum()
val revenueCount = FilmBoxOfficeTable.revenue.count()
val revenueReport = FilmBoxOfficeTable
.select(revenueSum, revenueCount, FilmBoxOfficeTable.region)
.groupBy(FilmBoxOfficeTable.region)
.map {
it[revenueSum] to it[revenueCount]
}
Statistics
Some databases provide aggregate functions specifically for statistics and Exposed provides support for four of these: .stdDevPop(), .stdDevSamp(), .varPop(), .varSamp(). The following example retrieves the population standard deviation of values stored in the revenue column:
val revenueStdDev = FilmBoxOfficeTable.revenue.stdDevPop()
val stdDev = FilmBoxOfficeTable
.select(revenueStdDev)
.singleOrNull()
?.get(revenueStdDev)
Custom functions
If you can't find your most loved function used in your database (as Exposed provides only basic support for classic SQL functions), you can define your own functions.
There are multiple options to define custom functions:
.function() simply wraps the column expression in parentheses with the string argument as the function name:
val sqrtRevenue = FilmBoxOfficeTable.revenue.function("SQRT")
// generates SQL: SQRT(SALES.REVENUE)
val sqrt = FilmBoxOfficeTable
.select(sqrtRevenue)
.singleOrNull()
?.get(sqrtRevenue)
Functions with additional parameters
The CustomFunction class accepts a function name as the first argument and the column type that should be used to handle its results as the second. After that, you can provide any amount of additional parameters separated by a comma:
Using one of these shortcuts, the example above could be simplified to:
val replacedStringTitle = CustomStringFunction(
"REPLACE", FilmBoxOfficeTable.title, stringLiteral("Title"), stringLiteral("New Title")
)
In the following example, CustomDateFunction is used in an H2 database to mimic its DATEADD() function in order to calculate a date three months before the current one. In is then chained with Exposed's built-in .month() function to return the month of the date found, so it can be used in a query:
Functions that require more complex query building
All functions in Exposed extend the abstract class Function, which takes a column type and allows overriding toQueryBuilder(). This is what CustomFunction actually does, which can be leveraged to create more complex queries.
For example, Exposed provides a .trim() function that removes leading and trailing whitespace from a String. In some databases (like H2 and MySQL), this is just the default behavior as specifiers can be provided to limit the trim to either leading or trailing. These databases also allow you to provide a specific substring other than spaces to remove. The custom function below supports this extended behavior:
package org.example.examples
import org.jetbrains.exposed.v1.core.Expression
import org.jetbrains.exposed.v1.core.Function
import org.jetbrains.exposed.v1.core.QueryBuilder
import org.jetbrains.exposed.v1.core.TextColumnType
import org.jetbrains.exposed.v1.core.append
enum class TrimSpecifier { BOTH, LEADING, TRAILING }
class CustomTrim<T : String?>(
val expression: Expression<T>,
val toRemove: Expression<T>?,
val trimSpecifier: TrimSpecifier
) : Function<String>(TextColumnType()) {
override fun toQueryBuilder(queryBuilder: QueryBuilder) {
queryBuilder {
append("TRIM(")
append(trimSpecifier.name)
toRemove?.let { +" $it" }
append(" FROM ", expression, ")")
}
}
}
fun <T : String?> Expression<T>.customTrim(
toRemove: Expression<T>? = null,
specifier: TrimSpecifier = TrimSpecifier.BOTH
): CustomTrim<T> = CustomTrim(this, toRemove, specifier)
This custom function can then be used to achieve the exact trim that is needed:
FilmBoxOfficeTable.insert {
it[title] = "Star Wars: The Phantom Menace - Episode I"
it[region] = "Spain"
it[revenue] = 99.toBigDecimal()
it[month] = REVENUE_MONTH
it[year] = REVENUE_YEAR
}
val leadingStarWarsTrim = FilmBoxOfficeTable.title.customTrim(stringLiteral("Star Wars:"), TrimSpecifier.LEADING)
val titleWithoutPrefix = FilmBoxOfficeTable.select(leadingStarWarsTrim).single()[leadingStarWarsTrim] // The Phantom Menace - Episode I
val trailingEpisodeTrim = FilmBoxOfficeTable.title.customTrim(stringLiteral("- Episode I"), TrimSpecifier.TRAILING)
val titleWithoutSuffix = FilmBoxOfficeTable.select(trailingEpisodeTrim).single()[trailingEpisodeTrim] // Star Wars: The Phantom Menace
Window functions
Window functions allow calculations across a set of table rows that are related to the current row.
Existing aggregate functions (like sum(), avg()) can be used, as well as new rank and value functions:
To use a window function, include the OVER clause by chaining .over() after the function call. A PARTITION BY and ORDER BY clause can be optionally chained using .partitionBy() and .orderBy(), taking multiple arguments:
val window1 = FilmBoxOfficeTable.revenue.sum()
.over()
.partitionBy(FilmBoxOfficeTable.year, FilmBoxOfficeTable.title)
.orderBy(FilmBoxOfficeTable.revenue)
val result1 = FilmBoxOfficeTable.select(window1).map { it[window1] }
val window2 = rowNumber()
.over()
.partitionBy(FilmBoxOfficeTable.year, FilmBoxOfficeTable.title)
.orderBy(FilmBoxOfficeTable.revenue)
val result2 = FilmBoxOfficeTable.select(window2).map { it[window2] }
val window3 = FilmBoxOfficeTable.revenue.sum()
.over()
.orderBy(FilmBoxOfficeTable.year to SortOrder.DESC, FilmBoxOfficeTable.title to SortOrder.ASC_NULLS_FIRST)
val result3 = FilmBoxOfficeTable.select(window3).map { it[window3] }
Frame clause functions, such as rows(), range(), and groups(), are also supported and take a WindowFrameBound option depending on the expected result: