WIP on tests

This commit is contained in:
2025-02-17 18:46:50 -05:00
parent b1a9910d50
commit 623e49243d
11 changed files with 646 additions and 199 deletions

View File

@@ -45,15 +45,15 @@ enum class AutoId {
* @param document The document whose need of an automatic ID should be determined
* @param idProp The name of the document property containing the ID
* @return `true` if the document needs an automatic ID, `false` if not
* @throws IllegalArgumentException If bad input prevents the determination
* @throws DocumentException If bad input prevents the determination
*/
fun <T> needsAutoId(strategy: AutoId, document: T, idProp: String): Boolean {
if (document == null) throw IllegalArgumentException("document cannot be null")
if (document == null) throw DocumentException("document cannot be null")
if (strategy == DISABLED) return false;
val id = document!!::class.memberProperties.find { it.name == idProp }
if (id == null) throw IllegalArgumentException("$idProp not found in document")
if (id == null) throw DocumentException("$idProp not found in document")
if (strategy == NUMBER) {
return when (id.returnType) {
@@ -61,7 +61,7 @@ enum class AutoId {
Short::class.createType() -> id.call(document) == 0.toShort()
Int::class.createType() -> id.call(document) == 0
Long::class.createType() -> id.call(document) == 0.toLong()
else -> throw IllegalArgumentException("$idProp was not a number; cannot auto-generate number ID")
else -> throw DocumentException("$idProp was not a number; cannot auto-generate number ID")
}
}
@@ -69,7 +69,7 @@ enum class AutoId {
return id.call(document) == ""
}
throw IllegalArgumentException("$idProp was not a string; cannot auto-generate UUID or random string")
throw DocumentException("$idProp was not a string; cannot auto-generate UUID or random string")
}
}
}

View File

@@ -10,16 +10,15 @@ class Comparison<T>(val op: Op, val value: T) {
/** Is the value for this comparison a numeric value? */
val isNumeric: Boolean
get() =
if (op == Op.IN || op == Op.BETWEEN) {
val values = value as? Collection<*>
if (values.isNullOrEmpty()) {
false
} else {
val first = values.elementAt(0)
first is Byte || first is Short || first is Int || first is Long
get() {
val toCheck = when (op) {
Op.IN -> {
val values = value as? Collection<*>
if (values.isNullOrEmpty()) "" else values.elementAt(0)
}
} else {
value is Byte || value is Short || value is Int || value is Long
Op.BETWEEN -> (value as Pair<*, *>).first
else -> value
}
return toCheck is Byte || toCheck is Short || toCheck is Int || toCheck is Long
}
}

View File

@@ -20,8 +20,8 @@ enum class Dialect {
*/
fun deriveFromConnectionString(connectionString: String): Dialect =
when {
connectionString.contains("sqlite") -> SQLITE
connectionString.contains("postgresql") -> POSTGRESQL
connectionString.contains(":sqlite:") -> SQLITE
connectionString.contains(":postgresql:") -> POSTGRESQL
else -> throw DocumentException("Cannot determine dialect from [$connectionString]")
}
}

View File

@@ -20,8 +20,11 @@ class Field<T> private constructor(
* @param paramName The parameter name to use for this field
* @return A new `Field` with the parameter name specified
*/
fun withParameterName(paramName: String): Field<T> =
Field(name, comparison, paramName, qualifier)
fun withParameterName(paramName: String): Field<T> {
if (!paramName.startsWith(':') && !paramName.startsWith('@'))
throw DocumentException("Parameter must start with : or @ ($paramName)")
return Field(name, comparison, paramName, qualifier)
}
/**
* Specify a qualifier (alias) for the document table
@@ -29,7 +32,7 @@ class Field<T> private constructor(
* @param alias The table alias for this field
* @return A new `Field` with the table qualifier specified
*/
fun withQualifier(alias: String): Field<T> =
fun withQualifier(alias: String) =
Field(name, comparison, parameterName, alias)
/**
@@ -74,16 +77,16 @@ class Field<T> private constructor(
in listOf(Op.EXISTS, Op.NOT_EXISTS) -> ""
Op.BETWEEN -> " ${parameterName}min AND ${parameterName}max"
Op.IN -> " ($inParameterNames)"
Op.IN_ARRAY -> if (dialect == Dialect.POSTGRESQL) " ARRAY['$inParameterNames']" else ""
Op.IN_ARRAY -> if (dialect == Dialect.POSTGRESQL) " ARRAY[$inParameterNames]" else ""
else -> " $parameterName"
}
@Suppress("UNCHECKED_CAST")
return if (dialect == Dialect.SQLITE && comparison.op == Op.IN_ARRAY) {
val (table, _) = comparison.value as? Pair<String, *> ?: throw DocumentException("InArray field invalid")
"EXISTS (SELECT 1 FROM json_each($table.data, '$.$name') WHERE value IN ($inParameterNames)"
"EXISTS (SELECT 1 FROM json_each($table.data, '$.$name') WHERE value IN ($inParameterNames))"
} else {
"$fieldPath ${comparison.op.sql} $criteria"
"$fieldPath ${comparison.op.sql}$criteria"
}
}
@@ -94,60 +97,66 @@ class Field<T> private constructor(
*
* @param name The name of the field to be compared
* @param value The value for the comparison
* @param paramName The parameter name for the field (optional, defaults to auto-generated)
* @return A `Field` with the given comparison
*/
fun <T> equal(name: String, value: T) =
Field(name, Comparison(Op.EQUAL, value))
fun <T> equal(name: String, value: T, paramName: String? = null) =
Field(name, Comparison(Op.EQUAL, value), paramName)
/**
* Create a field greater-than comparison
*
* @param name The name of the field to be compared
* @param value The value for the comparison
* @param paramName The parameter name for the field (optional, defaults to auto-generated)
* @return A `Field` with the given comparison
*/
fun <T> greater(name: String, value: T) =
Field(name, Comparison(Op.GREATER, value))
fun <T> greater(name: String, value: T, paramName: String? = null) =
Field(name, Comparison(Op.GREATER, value), paramName)
/**
* Create a field greater-than-or-equal-to comparison
*
* @param name The name of the field to be compared
* @param value The value for the comparison
* @param paramName The parameter name for the field (optional, defaults to auto-generated)
* @return A `Field` with the given comparison
*/
fun <T> greaterOrEqual(name: String, value: T) =
Field(name, Comparison(Op.GREATER_OR_EQUAL, value))
fun <T> greaterOrEqual(name: String, value: T, paramName: String? = null) =
Field(name, Comparison(Op.GREATER_OR_EQUAL, value), paramName)
/**
* Create a field less-than comparison
*
* @param name The name of the field to be compared
* @param value The value for the comparison
* @param paramName The parameter name for the field (optional, defaults to auto-generated)
* @return A `Field` with the given comparison
*/
fun <T> less(name: String, value: T) =
Field(name, Comparison(Op.LESS, value))
fun <T> less(name: String, value: T, paramName: String? = null) =
Field(name, Comparison(Op.LESS, value), paramName)
/**
* Create a field less-than-or-equal-to comparison
*
* @param name The name of the field to be compared
* @param value The value for the comparison
* @param paramName The parameter name for the field (optional, defaults to auto-generated)
* @return A `Field` with the given comparison
*/
fun <T> lessOrEqual(name: String, value: T) =
Field(name, Comparison(Op.LESS_OR_EQUAL, value))
fun <T> lessOrEqual(name: String, value: T, paramName: String? = null) =
Field(name, Comparison(Op.LESS_OR_EQUAL, value), paramName)
/**
* Create a field inequality comparison
*
* @param name The name of the field to be compared
* @param value The value for the comparison
* @param paramName The parameter name for the field (optional, defaults to auto-generated)
* @return A `Field` with the given comparison
*/
fun <T> notEqual(name: String, value: T) =
Field(name, Comparison(Op.NOT_EQUAL, value))
fun <T> notEqual(name: String, value: T, paramName: String? = null) =
Field(name, Comparison(Op.NOT_EQUAL, value), paramName)
/**
* Create a field range comparison
@@ -155,20 +164,22 @@ class Field<T> private constructor(
* @param name The name of the field to be compared
* @param minValue The lower value for the comparison
* @param maxValue The upper value for the comparison
* @param paramName The parameter name for the field (optional, defaults to auto-generated)
* @return A `Field` with the given comparison
*/
fun <T> between(name: String, minValue: T, maxValue: T) =
Field(name, Comparison(Op.BETWEEN, Pair(minValue, maxValue)))
fun <T> between(name: String, minValue: T, maxValue: T, paramName: String? = null) =
Field(name, Comparison(Op.BETWEEN, Pair(minValue, maxValue)), paramName)
/**
* Create a field where any values match (SQL `IN`)
*
* @param name The name of the field to be compared
* @param values The values for the comparison
* @param paramName The parameter name for the field (optional, defaults to auto-generated)
* @return A `Field` with the given comparison
*/
fun <T> any(name: String, values: List<T>) =
Field(name, Comparison(Op.IN, values))
fun <T> any(name: String, values: Collection<T>, paramName: String? = null) =
Field(name, Comparison(Op.IN, values), paramName)
/**
* Create a field where values should exist in a document's array
@@ -176,10 +187,11 @@ class Field<T> private constructor(
* @param name The name of the field to be compared
* @param tableName The name of the document table
* @param values The values for the comparison
* @param paramName The parameter name for the field (optional, defaults to auto-generated)
* @return A `Field` with the given comparison
*/
fun <T> inArray(name: String, tableName: String, values: List<T>) =
Field(name, Comparison(Op.IN_ARRAY, Pair(tableName, values)))
fun <T> inArray(name: String, tableName: String, values: Collection<T>, paramName: String? = null) =
Field(name, Comparison(Op.IN_ARRAY, Pair(tableName, values)), paramName)
fun exists(name: String) =
Field(name, Comparison(Op.EXISTS, ""))

View File

@@ -34,7 +34,7 @@ object Query {
* @param docId The ID value (optional; used for type determinations, string assumed if not provided)
*/
fun <TKey> byId(parameterName: String = ":id", docId: TKey? = null) =
byFields(listOf(Field.equal(Configuration.idField, docId ?: "").withParameterName(parameterName)))
byFields(listOf(Field.equal(Configuration.idField, docId ?: "", parameterName)))
/**
* Create a `WHERE` clause fragment to implement a JSON containment query (PostgreSQL only)