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()
}
}
}