package solutions.bitbadger.documents import java.sql.Connection import java.sql.PreparedStatement import java.sql.SQLException /** * 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.parameterName.isNullOrEmpty() && !listOf(Op.EXISTS, Op.NOT_EXISTS).contains(it.comparison.op)) { it.withParameterName(name.derive(null)) } else { it } } } /** * Create a parameter by encoding a JSON object * * @param name The parameter name * @param value The object to be encoded as JSON * @return A parameter with the value encoded */ inline fun json(name: String, value: T) = Parameter(name, ParameterType.JSON, Configuration.json.encodeToString(value)) /** * Add field parameters to the given set of parameters * * @param fields The fields being compared in the query * @param existing Any existing parameters for the query (optional, defaults to empty collection) * @return A collection of parameters for the query */ fun addFields(fields: Collection>, existing: MutableCollection> = mutableListOf()) = fields.fold(existing) { acc, field -> field.appendParameter(acc) } /** * 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 -> param.bind(stmt, index + 1) } } } catch (ex: SQLException) { throw DocumentException("Error creating query / binding parameters: ${ex.message}", ex) } } /** * Create parameters for field names to be removed from a document * * @param names The names of the fields to be removed * @param parameterName The parameter name to use for the query * @return A list of parameters to use for building the query */ fun fieldNames(names: Collection, parameterName: String = ":name") = when (Configuration.dialect("generate field name parameters")) { Dialect.POSTGRESQL -> listOf( Parameter(parameterName, ParameterType.STRING, names.joinToString(",").let { "{$it}" }) ) Dialect.SQLITE -> names.mapIndexed { index, name -> Parameter("$parameterName$index", ParameterType.STRING, name) } } }