Dynamic Queries
Runtime query generation with @SqlDynamic annotation
There are certain situations where ExoQuery cannot generate a query at compile-time. Most notably this happens when runtime values are used to choose a particular instance of SqlQuery or SqlExpression to be used. For example:
val someFlag: Boolean = someRuntimeLogic()
val q = sql {
if (someFlag) {
Table<Person>().filter { p -> p.name == "Joe" }
} else {
Table<Person>().filter { p -> p.name == "Jim" }
}
}
ExoQuery does not know the value of someFlag at compile-time and therefore cannot generate a query at that point. This means that ExoQuery needs to run the query at runtime as the capture block is executed. This is called a dynamic query. Dynamic queries are extremely flexible and ExoQuery is very good at handling them however there are a few caveats:
- Dynamic queries require the ExoQuery Query-Compiler to run with your runtime-code. Specifically, wherever you call the
.buildFor.SomeDatabase()function. - It can be problematic to call this code from performance-critical areas because their cost can be in the order of milliseconds (whereas non-dynamic queries have zero runtime cost since they are created inside the compiler). Be sure to test out how much time your dynamic-queries are taking if you have any concerns. Kotlinβs measureTime function is useful for this so long as you run it 5-10 times to let JIT do its work.
- You will not see dynamic-queries in the SQL log output because they are not generated until runtime, although you will see a message that the query is dynamic.
Dynamic queries effectively allow you pass around SqlQuery<T> and SqlExpression<T> objects without any restrictions or limitations. For example:
@SqlDynamic
fun filteredIds(robotsAllowed: Boolean, value: SqlExpression<String>) =
if (robotsAllowed)
sql {
Table<Person>().filter { p -> p.name == value }.map { p -> p.id }
union
Table<Robot>().filter { r -> r.model == value }.map { r -> r.id }
}
else
sql { Table<Person>().filter { p -> p.name == value }.map { r -> r.id } }
val q = sql {
Table<Tenants>().filter { c -> filteredIds(true, sql.expression { c.signatureName }) }
}
//> SELECT c.signatureName, c.rentalCode, c.moveInDate FROM Tenants c WHERE c.signatureName IN (SELECT p.id FROM Person p WHERE p.name = ? UNION SELECT r.id FROM Robot r WHERE r.model = ?)
Note several things:
- The
@SqlFragmentannotation is used to mark the function as a dynamic function, otherwise a parsing error will occur. - Captured functions will typically have one or two runtime flags and the other parameters going in should be
SqlQuery<T>orSqlExpression<T>objects. - Notice how above I used
capture.expression { c.signatureName }nested inside anothercaptureblock. This is required becausec.signatureNameis merely aString, not aSqlExpression<String>type.
Creating Query Fragments from Collections
Another advantage of dynamic queries is that you can use them to create query-fragments inside of collections. For example, the following code takes a list of possible names and creates a person.name == x || person.name == y || ... set of clauses from the list.
val possibleNames = listOf("Joe", "Jack")
@SqlDynamic
fun joinedClauses(p: SqlExpression<Person>) =
possibleNames.map { n -> sql.expression { p.use.name == param(n) } }
.reduce { a, b -> sql.expression { a.use || b.use } }
val filteredPeople = sql {
Table<Person>().filter { p -> joinedClauses(sql.expression { p }).use }
}
filteredPeople.buildFor.Postgres()
//> SELECT p.id, p.name, p.age FROM Person p WHERE p.name = ? OR p.name = ?
Again, notice how I wrapped p into capture.expression { p } to make it a SqlExpression<Person> type. What is being passed from filteredPeople to joinedClauses is literally the symbolic expression p.