package solutions.bitbadger.documents import java.sql.Connection import java.sql.PreparedStatement import java.sql.SQLException import java.sql.Types /** * Functions to assist with the creation and implementation of parameters for SQL queries * * @author Daniel J. Summers */ object Parameters { /** * Assign parameter names to any fields that do not have them assigned * * @param fields The collection of fields to be named * @return The collection of fields with parameter names assigned */ fun nameFields(fields: Collection>): Collection> { val name = ParameterName() return fields.map { if (it.name.isBlank()) it.withParameterName(name.derive(null)) else it } } /** * Replace the parameter names in the query with question marks * * @param query The query with named placeholders * @param parameters The parameters for the query * @return The query, with name parameters changed to `?`s */ fun replaceNamesInQuery(query: String, parameters: Collection>) = parameters.sortedByDescending { it.name.length }.fold(query) { acc, param -> acc.replace(param.name, "?") } /** * Apply the given parameters to the given query, returning a prepared statement * * @param conn The active JDBC connection * @param query The query * @param parameters The parameters for the query * @return A `PreparedStatement` with the parameter names replaced with `?` and parameter values bound * @throws DocumentException If parameter names are invalid or number value types are invalid */ fun apply(conn: Connection, query: String, parameters: Collection>): PreparedStatement { if (parameters.isEmpty()) return try { conn.prepareStatement(query) } catch (ex: SQLException) { throw DocumentException("Error preparing no-parameter query: ${ex.message}", ex) } val replacements = mutableListOf>>() parameters.sortedByDescending { it.name.length }.forEach { var startPos = query.indexOf(it.name) while (startPos > -1) { replacements.add(Pair(startPos, it)) startPos = query.indexOf(it.name, startPos + it.name.length + 1) } } return try { replaceNamesInQuery(query, parameters) .let { conn.prepareStatement(it) } .also { stmt -> replacements.sortedBy { it.first }.map { it.second }.forEachIndexed { index, param -> val idx = index + 1 when (param.type) { ParameterType.NUMBER -> { when (param.value) { null -> stmt.setNull(idx, Types.NULL) is Byte -> stmt.setByte(idx, param.value) is Short -> stmt.setShort(idx, param.value) is Int -> stmt.setInt(idx, param.value) is Long -> stmt.setLong(idx, param.value) else -> throw DocumentException( "Number parameter must be Byte, Short, Int, or Long (${param.value::class.simpleName})") } } ParameterType.STRING -> { when (param.value) { null -> stmt.setNull(idx, Types.NULL) is String -> stmt.setString(idx, param.value) else -> stmt.setString(idx, param.value.toString()) } } ParameterType.JSON -> stmt.setString(idx, Configuration.json.encodeToString(param.value)) } } } } catch (ex: SQLException) { throw DocumentException("Error creating query / binding parameters: ${ex.message}", ex) } } }