Fix count tests; use types for varying comparison values
This commit is contained in:
@@ -1,25 +1,43 @@
|
||||
package solutions.bitbadger.documents
|
||||
|
||||
/**
|
||||
* Information required to generate a JSON field comparison
|
||||
*/
|
||||
interface Comparison<T> {
|
||||
|
||||
/** The operation for the field comparison */
|
||||
val op: Op
|
||||
|
||||
val isNumeric: Boolean
|
||||
|
||||
/** The value against which the comparison will be made */
|
||||
val value: T
|
||||
|
||||
/** Whether the value should be considered numeric */
|
||||
val isNumeric: Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* A single-value comparison against a field in a JSON document
|
||||
* Function to determine if a value is numeric
|
||||
*
|
||||
* @property op The operation for the field comparison
|
||||
* @property value The value against which the comparison will be made
|
||||
* @param it The value in question
|
||||
* @return True if it is a numeric type, false if not
|
||||
*/
|
||||
class SingleComparison<T>(override val op: Op, override val value: T) : Comparison<T> {
|
||||
private fun <T> isNumeric(it: T) =
|
||||
it is Byte || it is Short || it is Int || it is Long
|
||||
|
||||
/** Is the value for this comparison a numeric value? */
|
||||
override val isNumeric: Boolean
|
||||
get() = value.let { it is Byte || it is Short || it is Int || it is Long }
|
||||
/**
|
||||
* A single-value comparison against a field in a JSON document
|
||||
*/
|
||||
class ComparisonSingle<T>(override val op: Op, override val value: T) : Comparison<T> {
|
||||
|
||||
init {
|
||||
when (op) {
|
||||
Op.BETWEEN, Op.IN, Op.IN_ARRAY ->
|
||||
throw DocumentException("Cannot use single comparison for multiple values")
|
||||
else -> { }
|
||||
}
|
||||
}
|
||||
|
||||
override val isNumeric = isNumeric(value)
|
||||
|
||||
override fun toString() =
|
||||
"$op $value"
|
||||
@@ -28,27 +46,23 @@ class SingleComparison<T>(override val op: Op, override val value: T) : Comparis
|
||||
/**
|
||||
* A range comparison against a field in a JSON document
|
||||
*/
|
||||
class BetweenComparison<T>(override val op: Op = Op.BETWEEN, override val value: Pair<T, T>) : Comparison<Pair<T, T>> {
|
||||
|
||||
override val isNumeric: Boolean
|
||||
get() = value.first.let { it is Byte || it is Short || it is Int || it is Long }
|
||||
class ComparisonBetween<T>(override val value: Pair<T, T>) : Comparison<Pair<T, T>> {
|
||||
override val op = Op.BETWEEN
|
||||
override val isNumeric = isNumeric(value.first)
|
||||
}
|
||||
|
||||
/**
|
||||
* A check within a collection of values
|
||||
*/
|
||||
class InComparison<T>(override val op: Op = Op.IN, override val value: Collection<T>) : Comparison<Collection<T>> {
|
||||
|
||||
override val isNumeric: Boolean
|
||||
get() = !value.isEmpty() && value.elementAt(0).let { it is Byte || it is Short || it is Int || it is Long }
|
||||
class ComparisonIn<T>(override val value: Collection<T>) : Comparison<Collection<T>> {
|
||||
override val op = Op.IN
|
||||
override val isNumeric = !value.isEmpty() && isNumeric(value.elementAt(0))
|
||||
}
|
||||
|
||||
/**
|
||||
* A check within a collection of values
|
||||
* A check within a collection of values against an array in a document
|
||||
*/
|
||||
class InArrayComparison<T>(override val op: Op = Op.IN_ARRAY, override val value: Pair<String, Collection<T>>) : Comparison<Pair<String, Collection<T>>> {
|
||||
|
||||
override val isNumeric: Boolean
|
||||
get() = !value.second.isEmpty() && value.second.elementAt(0)
|
||||
.let { it is Byte || it is Short || it is Int || it is Long }
|
||||
class ComparisonInArray<T>(override val value: Pair<String, Collection<T>>) : Comparison<Pair<String, Collection<T>>> {
|
||||
override val op = Op.IN_ARRAY
|
||||
override val isNumeric = false
|
||||
}
|
||||
|
||||
@@ -101,18 +101,18 @@ class Field<T> private constructor(
|
||||
fun appendParameter(existing: MutableCollection<Parameter<*>>): MutableCollection<Parameter<*>> {
|
||||
val typ = if (comparison.isNumeric) ParameterType.NUMBER else ParameterType.STRING
|
||||
when (comparison) {
|
||||
is BetweenComparison<*> -> {
|
||||
is ComparisonBetween<*> -> {
|
||||
existing.add(Parameter("${parameterName}min", typ, comparison.value.first))
|
||||
existing.add(Parameter("${parameterName}max", typ, comparison.value.second))
|
||||
}
|
||||
|
||||
is InComparison<*> -> {
|
||||
is ComparisonIn<*> -> {
|
||||
comparison.value.forEachIndexed { index, item ->
|
||||
existing.add(Parameter("${parameterName}_$index", typ, item))
|
||||
}
|
||||
}
|
||||
|
||||
is InArrayComparison<*> -> {
|
||||
is ComparisonInArray<*> -> {
|
||||
val mkString = Configuration.dialect("append parameters for InArray") == Dialect.POSTGRESQL
|
||||
// TODO: I think this is actually Pair<String, Collection<*>>
|
||||
comparison.value.second.forEachIndexed { index, item ->
|
||||
@@ -147,7 +147,7 @@ class Field<T> private constructor(
|
||||
* @return A `Field` with the given comparison
|
||||
*/
|
||||
fun <T> equal(name: String, value: T, paramName: String? = null) =
|
||||
Field(name, SingleComparison(Op.EQUAL, value), paramName)
|
||||
Field(name, ComparisonSingle(Op.EQUAL, value), paramName)
|
||||
|
||||
/**
|
||||
* Create a field greater-than comparison
|
||||
@@ -158,7 +158,7 @@ class Field<T> private constructor(
|
||||
* @return A `Field` with the given comparison
|
||||
*/
|
||||
fun <T> greater(name: String, value: T, paramName: String? = null) =
|
||||
Field(name, SingleComparison(Op.GREATER, value), paramName)
|
||||
Field(name, ComparisonSingle(Op.GREATER, value), paramName)
|
||||
|
||||
/**
|
||||
* Create a field greater-than-or-equal-to comparison
|
||||
@@ -169,7 +169,7 @@ class Field<T> private constructor(
|
||||
* @return A `Field` with the given comparison
|
||||
*/
|
||||
fun <T> greaterOrEqual(name: String, value: T, paramName: String? = null) =
|
||||
Field(name, SingleComparison(Op.GREATER_OR_EQUAL, value), paramName)
|
||||
Field(name, ComparisonSingle(Op.GREATER_OR_EQUAL, value), paramName)
|
||||
|
||||
/**
|
||||
* Create a field less-than comparison
|
||||
@@ -180,7 +180,7 @@ class Field<T> private constructor(
|
||||
* @return A `Field` with the given comparison
|
||||
*/
|
||||
fun <T> less(name: String, value: T, paramName: String? = null) =
|
||||
Field(name, SingleComparison(Op.LESS, value), paramName)
|
||||
Field(name, ComparisonSingle(Op.LESS, value), paramName)
|
||||
|
||||
/**
|
||||
* Create a field less-than-or-equal-to comparison
|
||||
@@ -191,7 +191,7 @@ class Field<T> private constructor(
|
||||
* @return A `Field` with the given comparison
|
||||
*/
|
||||
fun <T> lessOrEqual(name: String, value: T, paramName: String? = null) =
|
||||
Field(name, SingleComparison(Op.LESS_OR_EQUAL, value), paramName)
|
||||
Field(name, ComparisonSingle(Op.LESS_OR_EQUAL, value), paramName)
|
||||
|
||||
/**
|
||||
* Create a field inequality comparison
|
||||
@@ -202,7 +202,7 @@ class Field<T> private constructor(
|
||||
* @return A `Field` with the given comparison
|
||||
*/
|
||||
fun <T> notEqual(name: String, value: T, paramName: String? = null) =
|
||||
Field(name, SingleComparison(Op.NOT_EQUAL, value), paramName)
|
||||
Field(name, ComparisonSingle(Op.NOT_EQUAL, value), paramName)
|
||||
|
||||
/**
|
||||
* Create a field range comparison
|
||||
@@ -214,7 +214,7 @@ class Field<T> private constructor(
|
||||
* @return A `Field` with the given comparison
|
||||
*/
|
||||
fun <T> between(name: String, minValue: T, maxValue: T, paramName: String? = null) =
|
||||
Field(name, BetweenComparison(value = Pair(minValue, maxValue)), paramName)
|
||||
Field(name, ComparisonBetween(Pair(minValue, maxValue)), paramName)
|
||||
|
||||
/**
|
||||
* Create a field where any values match (SQL `IN`)
|
||||
@@ -225,7 +225,7 @@ class Field<T> private constructor(
|
||||
* @return A `Field` with the given comparison
|
||||
*/
|
||||
fun <T> any(name: String, values: Collection<T>, paramName: String? = null) =
|
||||
Field(name, InComparison(value = values), paramName)
|
||||
Field(name, ComparisonIn(values), paramName)
|
||||
|
||||
/**
|
||||
* Create a field where values should exist in a document's array
|
||||
@@ -237,16 +237,16 @@ class Field<T> private constructor(
|
||||
* @return A `Field` with the given comparison
|
||||
*/
|
||||
fun <T> inArray(name: String, tableName: String, values: Collection<T>, paramName: String? = null) =
|
||||
Field(name, InArrayComparison(value = Pair(tableName, values)), paramName)
|
||||
Field(name, ComparisonInArray(Pair(tableName, values)), paramName)
|
||||
|
||||
fun exists(name: String) =
|
||||
Field(name, SingleComparison(Op.EXISTS, ""))
|
||||
Field(name, ComparisonSingle(Op.EXISTS, ""))
|
||||
|
||||
fun notExists(name: String) =
|
||||
Field(name, SingleComparison(Op.NOT_EXISTS, ""))
|
||||
Field(name, ComparisonSingle(Op.NOT_EXISTS, ""))
|
||||
|
||||
fun named(name: String) =
|
||||
Field(name, SingleComparison(Op.EQUAL, ""))
|
||||
Field(name, ComparisonSingle(Op.EQUAL, ""))
|
||||
|
||||
fun nameToPath(name: String, dialect: Dialect, format: FieldFormat): String {
|
||||
val path = StringBuilder("data")
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package solutions.bitbadger.documents
|
||||
|
||||
import java.sql.PreparedStatement
|
||||
import java.sql.Types
|
||||
|
||||
/**
|
||||
* A parameter to use for a query
|
||||
*
|
||||
@@ -8,11 +11,45 @@ package solutions.bitbadger.documents
|
||||
* @property value The value of the parameter
|
||||
*/
|
||||
class Parameter<T>(val name: String, val type: ParameterType, val value: T) {
|
||||
|
||||
init {
|
||||
if (!name.startsWith(':') && !name.startsWith('@'))
|
||||
throw DocumentException("Name must start with : or @ ($name)")
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind this parameter to a prepared statement at the given index
|
||||
*
|
||||
* @param stmt The prepared statement to which this parameter should be bound
|
||||
* @param index The index (1-based) to which the parameter should be bound
|
||||
*/
|
||||
fun bind(stmt: PreparedStatement, index: Int) {
|
||||
when (type) {
|
||||
ParameterType.NUMBER -> {
|
||||
when (value) {
|
||||
null -> stmt.setNull(index, Types.NULL)
|
||||
is Byte -> stmt.setByte(index, value)
|
||||
is Short -> stmt.setShort(index, value)
|
||||
is Int -> stmt.setInt(index, value)
|
||||
is Long -> stmt.setLong(index, value)
|
||||
else -> throw DocumentException(
|
||||
"Number parameter must be Byte, Short, Int, or Long (${value!!::class.simpleName})"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ParameterType.STRING -> {
|
||||
when (value) {
|
||||
null -> stmt.setNull(index, Types.NULL)
|
||||
is String -> stmt.setString(index, value)
|
||||
else -> stmt.setString(index, value.toString())
|
||||
}
|
||||
}
|
||||
|
||||
ParameterType.JSON -> stmt.setObject(index, value as String, Types.OTHER)
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString() =
|
||||
"$type[$name] = $value"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ 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
|
||||
@@ -58,7 +57,7 @@ object Parameters {
|
||||
*/
|
||||
fun replaceNamesInQuery(query: String, parameters: Collection<Parameter<*>>) =
|
||||
parameters.sortedByDescending { it.name.length }.fold(query) { acc, param -> acc.replace(param.name, "?") }
|
||||
.also(::println)
|
||||
|
||||
/**
|
||||
* Apply the given parameters to the given query, returning a prepared statement
|
||||
*
|
||||
@@ -69,6 +68,7 @@ object Parameters {
|
||||
* @throws DocumentException If parameter names are invalid or number value types are invalid
|
||||
*/
|
||||
fun apply(conn: Connection, query: String, parameters: Collection<Parameter<*>>): PreparedStatement {
|
||||
|
||||
if (parameters.isEmpty()) return try {
|
||||
conn.prepareStatement(query)
|
||||
} catch (ex: SQLException) {
|
||||
@@ -88,34 +88,9 @@ object Parameters {
|
||||
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.setObject(idx, param.value as String, Types.OTHER)
|
||||
}
|
||||
}
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user