211 lines
8.1 KiB
Kotlin
211 lines
8.1 KiB
Kotlin
package solutions.bitbadger.documents
|
|
|
|
/**
|
|
* A field and its comparison
|
|
*
|
|
* @property name The name of the field in the JSON document
|
|
* @property comparison The comparison to apply against the field
|
|
* @property parameterName The name of the parameter to use in the query (optional, generated if missing)
|
|
* @property qualifier A table qualifier to use to address the `data` field (useful for multi-table queries)
|
|
*/
|
|
class Field<T> private constructor(
|
|
val name: String,
|
|
val comparison: Comparison<T>,
|
|
val parameterName: String? = null,
|
|
val qualifier: String? = null) {
|
|
|
|
/**
|
|
* Specify the parameter name for the field
|
|
*
|
|
* @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)
|
|
|
|
/**
|
|
* Specify a qualifier (alias) for the document table
|
|
*
|
|
* @param alias The table alias for this field
|
|
* @return A new `Field` with the table qualifier specified
|
|
*/
|
|
fun withQualifier(alias: String): Field<T> =
|
|
Field(name, comparison, parameterName, alias)
|
|
|
|
/**
|
|
* Get the path for this field
|
|
*
|
|
* @param dialect The SQL dialect to use for the path to the JSON field
|
|
* @param format Whether the value should be retrieved as JSON or SQL (optional, default SQL)
|
|
* @return The path for the field
|
|
*/
|
|
fun path(dialect: Dialect, format: FieldFormat = FieldFormat.SQL): String =
|
|
(if (qualifier == null) "" else "${qualifier}.") + nameToPath(name, dialect, format)
|
|
|
|
/** Parameters to bind each value of `IN` and `IN_ARRAY` operations */
|
|
private val inParameterNames: String
|
|
get() {
|
|
val values = if (comparison.op == Op.IN) {
|
|
comparison.value as Collection<*>
|
|
} else {
|
|
val parts = comparison.value as Pair<*, *>
|
|
parts.second as Collection<*>
|
|
}
|
|
return List(values.size) { idx -> "${parameterName}_$idx" }.joinToString(", ")
|
|
}
|
|
|
|
/**
|
|
* Create a `WHERE` clause fragment for this field
|
|
*
|
|
* @return The `WHERE` clause for this field
|
|
* @throws DocumentException If the field has no parameter name or the database dialect has not been set
|
|
*/
|
|
fun toWhere(): String {
|
|
if (parameterName == null && !listOf(Op.EXISTS, Op.NOT_EXISTS).contains(comparison.op))
|
|
throw DocumentException("Parameter for $name must be specified")
|
|
|
|
val dialect = Configuration.dialect("make field WHERE clause")
|
|
val fieldName = path(dialect, if (comparison.op == Op.IN_ARRAY) FieldFormat.JSON else FieldFormat.SQL)
|
|
val fieldPath = when (dialect) {
|
|
Dialect.POSTGRESQL -> if (comparison.isNumeric) "($fieldName)::numeric" else fieldName
|
|
Dialect.SQLITE -> fieldName
|
|
}
|
|
val criteria = when (comparison.op) {
|
|
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 ""
|
|
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)"
|
|
} else {
|
|
"$fieldPath ${comparison.op.sql} $criteria"
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
|
|
/**
|
|
* Create a field equality comparison
|
|
*
|
|
* @param name The name of the field to be compared
|
|
* @param value The value for the comparison
|
|
* @return A `Field` with the given comparison
|
|
*/
|
|
fun <T> equal(name: String, value: T) =
|
|
Field(name, Comparison(Op.EQUAL, value))
|
|
|
|
/**
|
|
* Create a field greater-than comparison
|
|
*
|
|
* @param name The name of the field to be compared
|
|
* @param value The value for the comparison
|
|
* @return A `Field` with the given comparison
|
|
*/
|
|
fun <T> greater(name: String, value: T) =
|
|
Field(name, Comparison(Op.GREATER, value))
|
|
|
|
/**
|
|
* 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
|
|
* @return A `Field` with the given comparison
|
|
*/
|
|
fun <T> greaterOrEqual(name: String, value: T) =
|
|
Field(name, Comparison(Op.GREATER_OR_EQUAL, value))
|
|
|
|
/**
|
|
* Create a field less-than comparison
|
|
*
|
|
* @param name The name of the field to be compared
|
|
* @param value The value for the comparison
|
|
* @return A `Field` with the given comparison
|
|
*/
|
|
fun <T> less(name: String, value: T) =
|
|
Field(name, Comparison(Op.LESS, value))
|
|
|
|
/**
|
|
* 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
|
|
* @return A `Field` with the given comparison
|
|
*/
|
|
fun <T> lessOrEqual(name: String, value: T) =
|
|
Field(name, Comparison(Op.LESS_OR_EQUAL, value))
|
|
|
|
/**
|
|
* Create a field inequality comparison
|
|
*
|
|
* @param name The name of the field to be compared
|
|
* @param value The value for the comparison
|
|
* @return A `Field` with the given comparison
|
|
*/
|
|
fun <T> notEqual(name: String, value: T) =
|
|
Field(name, Comparison(Op.NOT_EQUAL, value))
|
|
|
|
/**
|
|
* Create a field range comparison
|
|
*
|
|
* @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
|
|
* @return A `Field` with the given comparison
|
|
*/
|
|
fun <T> between(name: String, minValue: T, maxValue: T) =
|
|
Field(name, Comparison(Op.BETWEEN, Pair(minValue, maxValue)))
|
|
|
|
/**
|
|
* 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
|
|
* @return A `Field` with the given comparison
|
|
*/
|
|
fun <T> any(name: String, values: List<T>) =
|
|
Field(name, Comparison(Op.IN, values))
|
|
|
|
/**
|
|
* Create a field where values should exist in a document's array
|
|
*
|
|
* @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
|
|
* @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 exists(name: String) =
|
|
Field(name, Comparison(Op.EXISTS, ""))
|
|
|
|
fun notExists(name: String) =
|
|
Field(name, Comparison(Op.NOT_EXISTS, ""))
|
|
|
|
fun named(name: String) =
|
|
Field(name, Comparison(Op.EQUAL, ""))
|
|
|
|
fun nameToPath(name: String, dialect: Dialect, format: FieldFormat): String {
|
|
val path = StringBuilder("data")
|
|
val extra = if (format == FieldFormat.SQL) ">" else ""
|
|
if (name.indexOf('.') > -1) {
|
|
if (dialect == Dialect.POSTGRESQL) {
|
|
path.append("#>", extra, "'{", name.replace('.', ','), "}'")
|
|
} else {
|
|
val names = name.split('.').toMutableList()
|
|
val last = names.removeLast()
|
|
names.forEach { path.append("->'", it, "'") }
|
|
path.append("->", extra, "'", last, "'")
|
|
}
|
|
} else {
|
|
path.append("->", extra, "'", name, "'")
|
|
}
|
|
return path.toString()
|
|
}
|
|
}
|
|
} |