Advanced Features

Free Blocks

Using custom SQL with free blocks for UDFs and advanced syntax

Calling UDFs

In situations where you need to use a SQL UDF that is available directly on the database, or when you need to use custom SQL syntax that is not supported by ExoQuery, you can use a free block.

val q = sql {
  Table<Person>().filter { p -> free("mySpecialDatabaseUDF(${p.name})") == "Joe" }
}
//> SELECT p.id, p.name, p.age FROM Person p WHERE mySpecialDatabaseUDF(p.name) = 'Joe'

You can pass param-calls into the free-block as well.

val myRuntimeVar = ...
val q = sql {
  Table<Person>().filter { p -> p.name == free("mySpecialDatabaseUDF(${param(myRuntimeVar)})") }
}
//> SELECT p.id, p.name, p.age FROM Person p WHERE p.name = mySpecialDatabaseUDF(?)

Enriching Queries

Free blocks are also useful for adding information before/after an entire query. For example:

val q = sql {
  free("${Table<Person>().filter { p -> p.name == "Joe" }} FOR UPDATE").asPure<SqlQuery<Person>>()
}
//> SELECT p.id, p.name, p.age FROM Person p WHERE p.name = 'Joe' FOR UPDATE

This is technique is quite powerful when combined with SQL Fragment Functions to abstract out logic:

@SqlFragment
fun <T : Person> forUpdate(v: SqlQuery<T>) = sql {
    free("${v} FOR UPDATE").asPure<SqlQuery<T>>()
  }

val q = sql {
  forUpdate(Table<Person>().filter { p -> p.age > 21 })
}
//> (SELECT p.id, p.name, p.age FROM Person p WHERE p.age > 21) FOR UPDATE

Enriching Actions

Free blocks can even be used with Action (i.e. insert, update, delete) statements:

val q = sql {
  insert<Person> { setParams(Person(1, "Joe", "Bloggs", 111)) }
}
val qq = sql {
  free("SET IDENTITY_INSERT Person ON ${q} SET IDENTITY_INSERT Person OFF").asPure<SqlAction<Person, Long>>()
}
qq.build<SqlServerDialect>().runOn(ctx)
//> SET IDENTITY_INSERT Person ON  INSERT INTO Person (id, name, age, companyId) VALUES (?, ?, ?, ?) SET IDENTITY_INSERT Person OFF

free() Return Types by Context

The free() function behaves differently depending on where it's used:

Inside sql.select { } block

free(...).asPure<T>() returns T directly (not SqlExpression<T>):

val q = sql.select {
    val p = from(Table<Person>())
    val upper = free("UPPER(${p.name})").asPure<String>()  // upper is String
    groupBy(upper)  // Works directly, no .use needed
    Pair(upper, count())
}

Inside sql.expression { } block

free(...).asPure<T>() returns T which becomes part of the SqlExpression<T>:

@SqlFragment
fun monthOf(date: String): SqlExpression<String> = sql.expression {
    free("strftime('%Y-%m', ${date})").asPure<String>()
}

// When used:
val month = monthOf(i.paidAt).use  // .use required to extract String

Inside @SqlFragment functions

Fragments returning SqlExpression<T> require .use at call sites:

@SqlFragment
fun coalesce(value: Long?, default: Long): SqlExpression<Long> = sql.expression {
    free("COALESCE(${value}, ${default})").asPure<Long>()
}

// Usage in sql.select:
val netAmount = grossAmount - coalesce(refund, 0L).use  // .use required

Quick Reference

Locationfree().asPure<T>() returnsNeed .use at call site?
sql.select { }TNo
sql.expression { }T (wrapped in SqlExpression)Yes
@SqlFragment returning SqlExpression<T>SqlExpression<T>Yes
@SqlFragment returning SqlQuery<T>SqlQuery<T>No (use with from/join)