Last active
February 15, 2022 10:13
-
-
Save peterbb/8f2e237fa72627fe072ebf7ae796e3f2 to your computer and use it in GitHub Desktop.
Named, typed parameters for java.sql.PreparedStatement in Kotlin
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
object GetUser: Template() { | |
var id by SqlInt | |
var name by SqlString | |
override fun query() = """ | |
(select * from users where name = $name and id = $id) | |
union | |
(select * from deleted_user where id = $id) | |
""" | |
} | |
fun java.sql.Connection.getUser(name: String, id: Int): java.sql.PreparedStatement = | |
prepareStatement(GetUser) { | |
parameter.name = inject(name) | |
parameter.id = inject(id) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.sql.Connection | |
import java.sql.PreparedStatement | |
import kotlin.reflect.KProperty | |
abstract class Template { | |
abstract fun query(): String | |
private var index = 1 | |
private val variables = mutableMapOf<String, MutableList<Int>>() | |
private var compiledQuery: String? = null | |
private fun <T> getValue(name: String): Parameter<T> { | |
require(compiledQuery == null) | |
variables | |
.getOrPut(name) { mutableListOf() } | |
.add(index++) | |
return Parameter.Placeholder() | |
} | |
private fun <T> setValue( | |
name: String, | |
setter: (PreparedStatement, Int, T) -> Unit, | |
parameter: Parameter<T> | |
) { | |
require(compiledQuery != null) | |
when (parameter) { | |
is Parameter.Placeholder -> error("nope") | |
is Parameter.Value -> { | |
val indexes = variables[name] ?: error("'$name' not set") | |
for (i in indexes) { | |
setter(parameter.statement, i, parameter.value) | |
} | |
} | |
} | |
} | |
open class ParameterDelegate<T : Any>( | |
private val setter: (PreparedStatement, Int, T) -> Unit | |
) { | |
operator fun getValue(template: Template, property: KProperty<*>): Parameter<T> { | |
return template.getValue(property.name) | |
} | |
operator fun setValue(template: Template, property: KProperty<*>, parameter: Parameter<T>) { | |
template.setValue(property.name, setter, parameter) | |
} | |
} | |
class ParameterInjectionContext<T : Template>( | |
val parameter: T, | |
private val preparedStatement: PreparedStatement, | |
) { | |
fun <X> inject(x: X) = Parameter.Value(x, preparedStatement) | |
} | |
sealed interface Parameter<T> { | |
class Placeholder<T> : Parameter<T> { | |
override fun toString(): String = "?" | |
} | |
class Value<T>( | |
val value: T, | |
val statement: PreparedStatement, | |
) : Parameter<T> | |
} | |
companion object { | |
fun <T : Template> Connection.prepareStatement( | |
template: T, | |
parameterInjection: ParameterInjectionContext<T>.() -> Unit | |
): PreparedStatement { | |
if (template.compiledQuery == null) { | |
template.compiledQuery = template.query() | |
} | |
return this.prepareStatement(template.compiledQuery) | |
.also { | |
try { | |
ParameterInjectionContext(template, it).parameterInjection() | |
} catch (e: Exception) { | |
it.close() | |
throw e | |
} | |
} | |
} | |
} | |
} | |
object SqlString : Template.ParameterDelegate<String>(PreparedStatement::setString) | |
object SqlInt : Template.ParameterDelegate<Int>(PreparedStatement::setInt) | |
object SqlUUID : Template.ParameterDelegate<java.util.UUID>(PreparedStatement::setObject) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment