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 private constructor( val name: String, val comparison: Comparison, 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 = 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 = 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 ?: 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 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 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 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 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 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 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 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 any(name: String, values: List) = 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 inArray(name: String, tableName: String, values: List) = 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() } } }