I needed something to map named SQL parameters into indices for JDBC queries so I wrote the ParameterMap
class that takes an SQL query, searchers for parameters prefixed with a colon :
and while replacing each instance it counts their indices adding them to the map as a list associated with each name. There is also an extension function forEach
that passes one index at a time to the lambda where the actual parameters are set.
See also the follow-up.
class ParameterMap(sql: String) {
private val parameterRegex = Regex("(:([a-z0-9_]+))", RegexOption.IGNORE_CASE)
private val map = mutableMapOf<String, List<Int>>()
val sqlWithQuestionMarks: String
init {
var index = 1 // <-- JDBC starts counting with 1
sqlWithQuestionMarks = parameterRegex.replace(sql) { match ->
val key = match.groups[2]!!.value.lowercase()
map.computeIfPresent(key) { _, indexes -> indexes.plus(index++) }
map.computeIfAbsent(key) { mutableListOf(index++) }
return@replace "?"
}
}
operator fun get(name: String) = map.getValue(name.lowercase())
override fun toString() = sqlWithQuestionMarks
}
fun ParameterMap.forEach(name: String, block: (Int) -> Unit) {
for (index in this[name]) block(index)
}
Example:
select *
from person
where
(nullif(:first_name, '') is null or first_name = :first_name) and
(nullif(:last_name, '') is null or last_name = :last_name)
limit 100;
val parameterMap = ParameterMap(File("sql/select.sql").readText())
val selectStatement = sqliteConnection.prepareStatement(parameterMap.sqlWithQuestionMarks)
parameterMap.forEach("first_name") {
selectStatement.setString(it, "John")
}
parameterMap.forEach("last_name") {
selectStatement.setString(it, "Doe")
}
What do you think of this helper? Can it be made more Kotlin-ish?
IllegalStateException
or something when there's un-replaced placeholders. This way, you prevent sending invalid SQL into the database. SQLite3 seems to support the:placeholder
style natively, so, you will see weird side effects from the SQLite3 side. \$\endgroup\$