WIP on count impl; reworking comparisons
This commit is contained in:
@@ -1,27 +1,54 @@
|
||||
package solutions.bitbadger.documents
|
||||
|
||||
interface Comparison<T> {
|
||||
|
||||
val op: Op
|
||||
|
||||
val isNumeric: Boolean
|
||||
|
||||
val value: T
|
||||
}
|
||||
|
||||
/**
|
||||
* A comparison against a field in a JSON document
|
||||
* A single-value comparison against a field in a JSON document
|
||||
*
|
||||
* @property op The operation for the field comparison
|
||||
* @property value The value against which the comparison will be made
|
||||
*/
|
||||
class Comparison<T>(val op: Op, val value: T) {
|
||||
class SingleComparison<T>(override val op: Op, override val value: T) : Comparison<T> {
|
||||
|
||||
/** Is the value for this comparison a numeric value? */
|
||||
val isNumeric: Boolean
|
||||
get() {
|
||||
val toCheck = when (op) {
|
||||
Op.IN -> {
|
||||
val values = value as? Collection<*>
|
||||
if (values.isNullOrEmpty()) "" else values.elementAt(0)
|
||||
}
|
||||
Op.BETWEEN -> (value as Pair<*, *>).first
|
||||
else -> value
|
||||
}
|
||||
return toCheck is Byte || toCheck is Short || toCheck is Int || toCheck is Long
|
||||
}
|
||||
override val isNumeric: Boolean
|
||||
get() = value.let { it is Byte || it is Short || it is Int || it is Long }
|
||||
|
||||
override fun toString() =
|
||||
"$op $value"
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 }
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 }
|
||||
}
|
||||
|
||||
/**
|
||||
* A check within a collection of values
|
||||
*/
|
||||
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 }
|
||||
}
|
||||
|
||||
@@ -72,6 +72,16 @@ fun Connection.ensureTable(tableName: String) =
|
||||
fun Connection.ensureFieldIndex(tableName: String, indexName: String, fields: Collection<String>) =
|
||||
Definition.ensureFieldIndex(tableName, indexName, fields, this)
|
||||
|
||||
/**
|
||||
* Create a document index on a table (PostgreSQL only)
|
||||
*
|
||||
* @param tableName The table to be indexed (may include schema)
|
||||
* @param indexType The type of index to ensure
|
||||
* @throws DocumentException If called on a SQLite connection
|
||||
*/
|
||||
fun Connection.ensureDocumentIndex(tableName: String, indexType: DocumentIndex) =
|
||||
Definition.ensureDocumentIndex(tableName, indexType, this)
|
||||
|
||||
// ~~~ DOCUMENT MANIPULATION QUERIES ~~~
|
||||
|
||||
/**
|
||||
@@ -94,6 +104,38 @@ inline fun <reified TDoc> Connection.insert(tableName: String, document: TDoc) =
|
||||
fun Connection.countAll(tableName: String) =
|
||||
Count.all(tableName, this)
|
||||
|
||||
/**
|
||||
* Count documents using a field comparison
|
||||
*
|
||||
* @param tableName The name of the table in which documents should be counted
|
||||
* @param fields The fields which should be compared
|
||||
* @param howMatched How the fields should be matched
|
||||
* @return A count of the matching documents in the table
|
||||
*/
|
||||
fun Connection.countByFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
|
||||
Count.byFields(tableName, fields, howMatched, this)
|
||||
|
||||
/**
|
||||
* Count documents using a JSON containment query (PostgreSQL only)
|
||||
*
|
||||
* @param tableName The name of the table in which documents should be counted
|
||||
* @param criteria The object for which JSON containment should be checked
|
||||
* @return A count of the matching documents in the table
|
||||
* @throws DocumentException If called on a SQLite connection
|
||||
*/
|
||||
inline fun <reified T> Connection.countByContains(tableName: String, criteria: T) =
|
||||
Count.byContains(tableName, criteria, this)
|
||||
|
||||
/**
|
||||
* Count documents using a JSON containment query (PostgreSQL only)
|
||||
*
|
||||
* @param tableName The name of the table in which documents should be counted
|
||||
* @param path The JSON path comparison to match
|
||||
* @return A count of the matching documents in the table
|
||||
* @throws DocumentException If called on a SQLite connection
|
||||
*/
|
||||
fun Connection.countByJsonPath(tableName: String, path: String) =
|
||||
Count.byJsonPath(tableName, path, this)
|
||||
|
||||
// ~~~ DOCUMENT RETRIEVAL QUERIES ~~~
|
||||
|
||||
@@ -134,4 +176,4 @@ fun <TKey> Connection.byId(tableName: String, docId: TKey) =
|
||||
* @param howMatched How the fields should be matched
|
||||
*/
|
||||
fun Connection.deleteByFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
|
||||
Delete.byField(tableName, fields, howMatched, this)
|
||||
Delete.byFields(tableName, fields, howMatched, this)
|
||||
|
||||
@@ -26,4 +26,88 @@ object Count {
|
||||
*/
|
||||
fun all(tableName: String) =
|
||||
Configuration.dbConn().use { all(tableName, it) }
|
||||
|
||||
/**
|
||||
* Count documents using a field comparison
|
||||
*
|
||||
* @param tableName The name of the table in which documents should be counted
|
||||
* @param fields The fields which should be compared
|
||||
* @param howMatched How the fields should be matched
|
||||
* @param conn The connection on which the deletion should be executed
|
||||
* @return A count of the matching documents in the table
|
||||
*/
|
||||
fun byFields(
|
||||
tableName: String,
|
||||
fields: Collection<Field<*>>,
|
||||
howMatched: FieldMatch? = null,
|
||||
conn: Connection
|
||||
): Long {
|
||||
val named = Parameters.nameFields(fields)
|
||||
return conn.customScalar(
|
||||
Count.byFields(tableName, named, howMatched),
|
||||
Parameters.addFields(named),
|
||||
Results::toCount
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Count documents using a field comparison
|
||||
*
|
||||
* @param tableName The name of the table in which documents should be counted
|
||||
* @param fields The fields which should be compared
|
||||
* @param howMatched How the fields should be matched
|
||||
* @return A count of the matching documents in the table
|
||||
*/
|
||||
fun byFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
|
||||
Configuration.dbConn().use { byFields(tableName, fields, howMatched, it) }
|
||||
|
||||
/**
|
||||
* Count documents using a JSON containment query (PostgreSQL only)
|
||||
*
|
||||
* @param tableName The name of the table in which documents should be counted
|
||||
* @param criteria The object for which JSON containment should be checked
|
||||
* @param conn The connection on which the deletion should be executed
|
||||
* @return A count of the matching documents in the table
|
||||
* @throws DocumentException If called on a SQLite connection
|
||||
*/
|
||||
inline fun <reified T> byContains(tableName: String, criteria: T, conn: Connection) =
|
||||
conn.customScalar(Count.byContains(tableName), listOf(Parameters.json(":criteria", criteria)), Results::toCount)
|
||||
|
||||
/**
|
||||
* Count documents using a JSON containment query (PostgreSQL only)
|
||||
*
|
||||
* @param tableName The name of the table in which documents should be counted
|
||||
* @param criteria The object for which JSON containment should be checked
|
||||
* @return A count of the matching documents in the table
|
||||
* @throws DocumentException If called on a SQLite connection
|
||||
*/
|
||||
inline fun <reified T> byContains(tableName: String, criteria: T) =
|
||||
Configuration.dbConn().use { byContains(tableName, criteria, it) }
|
||||
|
||||
/**
|
||||
* Count documents using a JSON containment query (PostgreSQL only)
|
||||
*
|
||||
* @param tableName The name of the table in which documents should be counted
|
||||
* @param path The JSON path comparison to match
|
||||
* @param conn The connection on which the deletion should be executed
|
||||
* @return A count of the matching documents in the table
|
||||
* @throws DocumentException If called on a SQLite connection
|
||||
*/
|
||||
fun byJsonPath(tableName: String, path: String, conn: Connection) =
|
||||
conn.customScalar(
|
||||
Count.byJsonPath(tableName),
|
||||
listOf(Parameter(":path", ParameterType.STRING, path)),
|
||||
Results::toCount
|
||||
)
|
||||
|
||||
/**
|
||||
* Count documents using a JSON containment query (PostgreSQL only)
|
||||
*
|
||||
* @param tableName The name of the table in which documents should be counted
|
||||
* @param path The JSON path comparison to match
|
||||
* @return A count of the matching documents in the table
|
||||
* @throws DocumentException If called on a SQLite connection
|
||||
*/
|
||||
fun byJsonPath(tableName: String, path: String) =
|
||||
Configuration.dbConn().use { byJsonPath(tableName, path, it) }
|
||||
}
|
||||
|
||||
@@ -41,11 +41,32 @@ object Definition {
|
||||
|
||||
/**
|
||||
* Create an index on field(s) within documents in the specified table if necessary
|
||||
*
|
||||
* @param tableName The table to be indexed (may include schema)
|
||||
* @param indexName The name of the index to create
|
||||
* @param fields One or more fields to be indexed<
|
||||
* @param conn The connection on which the query should be executed
|
||||
*/
|
||||
fun ensureFieldIndex(tableName: String, indexName: String, fields: Collection<String>) =
|
||||
Configuration.dbConn().use { ensureFieldIndex(tableName, indexName, fields, it) }
|
||||
|
||||
/**
|
||||
* Create a document index on a table (PostgreSQL only)
|
||||
*
|
||||
* @param tableName The table to be indexed (may include schema)
|
||||
* @param indexType The type of index to ensure
|
||||
* @param conn The connection on which the query should be executed
|
||||
* @throws DocumentException If called on a SQLite connection
|
||||
*/
|
||||
fun ensureDocumentIndex(tableName: String, indexType: DocumentIndex, conn: Connection) =
|
||||
conn.customNonQuery(Definition.ensureDocumentIndexOn(tableName, indexType))
|
||||
|
||||
/**
|
||||
* Create a document index on a table (PostgreSQL only)
|
||||
*
|
||||
* @param tableName The table to be indexed (may include schema)
|
||||
* @param indexType The type of index to ensure
|
||||
* @throws DocumentException If called on a SQLite connection
|
||||
*/
|
||||
fun ensureDocumentIndex(tableName: String, indexType: DocumentIndex) =
|
||||
Configuration.dbConn().use { ensureDocumentIndex(tableName, indexType, it) }
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ object Delete {
|
||||
* @param howMatched How the fields should be matched
|
||||
* @param conn The connection on which the deletion should be executed
|
||||
*/
|
||||
fun byField(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null, conn: Connection) {
|
||||
fun byFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null, conn: Connection) {
|
||||
val named = Parameters.nameFields(fields)
|
||||
conn.customNonQuery(Delete.byFields(tableName, named, howMatched), Parameters.addFields(named))
|
||||
}
|
||||
@@ -50,6 +50,6 @@ object Delete {
|
||||
* @param fields The fields which should be compared
|
||||
* @param howMatched How the fields should be matched
|
||||
*/
|
||||
fun byField(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
|
||||
Configuration.dbConn().use { byField(tableName, fields, howMatched, it) }
|
||||
fun byFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
|
||||
Configuration.dbConn().use { byFields(tableName, fields, howMatched, it) }
|
||||
}
|
||||
|
||||
13
src/main/kotlin/DocumentIndex.kt
Normal file
13
src/main/kotlin/DocumentIndex.kt
Normal file
@@ -0,0 +1,13 @@
|
||||
package solutions.bitbadger.documents
|
||||
|
||||
/**
|
||||
* The type of index to generate for the document
|
||||
*/
|
||||
enum class DocumentIndex(val sql: String) {
|
||||
|
||||
/** A GIN index with standard operations (all operators supported) */
|
||||
FULL(""),
|
||||
|
||||
/** A GIN index with JSONPath operations (optimized for @>, @?, @@ operators) */
|
||||
OPTIMIZED(" jsonb_path_ops")
|
||||
}
|
||||
@@ -92,6 +92,47 @@ class Field<T> private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append the parameters required for this field
|
||||
*
|
||||
* @param existing The existing parameters
|
||||
* @return The collection with the necessary parameters appended
|
||||
*/
|
||||
fun appendParameter(existing: MutableCollection<Parameter<*>>): MutableCollection<Parameter<*>> {
|
||||
val typ = if (comparison.isNumeric) ParameterType.NUMBER else ParameterType.STRING
|
||||
when (comparison) {
|
||||
is BetweenComparison<*> -> {
|
||||
existing.add(Parameter("${parameterName}min", typ, comparison.value.first))
|
||||
existing.add(Parameter("${parameterName}max", typ, comparison.value.second))
|
||||
}
|
||||
|
||||
is InComparison<*> -> {
|
||||
comparison.value.forEachIndexed { index, item ->
|
||||
existing.add(Parameter("${parameterName}_$index", typ, item))
|
||||
}
|
||||
}
|
||||
|
||||
is InArrayComparison<*> -> {
|
||||
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 ->
|
||||
if (mkString) {
|
||||
existing.add(Parameter("${parameterName}_$index", ParameterType.STRING, "$item"))
|
||||
} else {
|
||||
existing.add(Parameter("${parameterName}_$index", typ, item))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
if (comparison.op != Op.EXISTS && comparison.op != Op.NOT_EXISTS) {
|
||||
existing.add(Parameter(parameterName!!, typ, comparison.value))
|
||||
}
|
||||
}
|
||||
}
|
||||
return existing
|
||||
}
|
||||
|
||||
override fun toString() =
|
||||
"Field ${parameterName ?: "<unnamed>"} $comparison${qualifier?.let { " (qualifier $it)"} ?: ""}"
|
||||
|
||||
@@ -106,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, Comparison(Op.EQUAL, value), paramName)
|
||||
Field(name, SingleComparison(Op.EQUAL, value), paramName)
|
||||
|
||||
/**
|
||||
* Create a field greater-than comparison
|
||||
@@ -117,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, Comparison(Op.GREATER, value), paramName)
|
||||
Field(name, SingleComparison(Op.GREATER, value), paramName)
|
||||
|
||||
/**
|
||||
* Create a field greater-than-or-equal-to comparison
|
||||
@@ -128,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, Comparison(Op.GREATER_OR_EQUAL, value), paramName)
|
||||
Field(name, SingleComparison(Op.GREATER_OR_EQUAL, value), paramName)
|
||||
|
||||
/**
|
||||
* Create a field less-than comparison
|
||||
@@ -139,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, Comparison(Op.LESS, value), paramName)
|
||||
Field(name, SingleComparison(Op.LESS, value), paramName)
|
||||
|
||||
/**
|
||||
* Create a field less-than-or-equal-to comparison
|
||||
@@ -150,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, Comparison(Op.LESS_OR_EQUAL, value), paramName)
|
||||
Field(name, SingleComparison(Op.LESS_OR_EQUAL, value), paramName)
|
||||
|
||||
/**
|
||||
* Create a field inequality comparison
|
||||
@@ -161,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, Comparison(Op.NOT_EQUAL, value), paramName)
|
||||
Field(name, SingleComparison(Op.NOT_EQUAL, value), paramName)
|
||||
|
||||
/**
|
||||
* Create a field range comparison
|
||||
@@ -173,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, Comparison(Op.BETWEEN, Pair(minValue, maxValue)), paramName)
|
||||
Field(name, BetweenComparison(value = Pair(minValue, maxValue)), paramName)
|
||||
|
||||
/**
|
||||
* Create a field where any values match (SQL `IN`)
|
||||
@@ -184,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, Comparison(Op.IN, values), paramName)
|
||||
Field(name, InComparison(value = values), paramName)
|
||||
|
||||
/**
|
||||
* Create a field where values should exist in a document's array
|
||||
@@ -196,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, Comparison(Op.IN_ARRAY, Pair(tableName, values)), paramName)
|
||||
Field(name, InArrayComparison(value = Pair(tableName, values)), paramName)
|
||||
|
||||
fun exists(name: String) =
|
||||
Field(name, Comparison(Op.EXISTS, ""))
|
||||
Field(name, SingleComparison(Op.EXISTS, ""))
|
||||
|
||||
fun notExists(name: String) =
|
||||
Field(name, Comparison(Op.NOT_EXISTS, ""))
|
||||
Field(name, SingleComparison(Op.NOT_EXISTS, ""))
|
||||
|
||||
fun named(name: String) =
|
||||
Field(name, Comparison(Op.EQUAL, ""))
|
||||
Field(name, SingleComparison(Op.EQUAL, ""))
|
||||
|
||||
fun nameToPath(name: String, dialect: Dialect, format: FieldFormat): String {
|
||||
val path = StringBuilder("data")
|
||||
|
||||
@@ -46,22 +46,8 @@ object Parameters {
|
||||
* @param existing Any existing parameters for the query (optional, defaults to empty collection)
|
||||
* @return A collection of parameters for the query
|
||||
*/
|
||||
fun addFields(
|
||||
fields: Collection<Field<*>>,
|
||||
existing: MutableCollection<Parameter<*>> = mutableListOf()
|
||||
): MutableCollection<Parameter<*>> {
|
||||
existing.addAll(
|
||||
fields
|
||||
.filter { it.comparison.op != Op.EXISTS && it.comparison.op != Op.NOT_EXISTS }
|
||||
.map {
|
||||
Parameter(
|
||||
it.parameterName!!,
|
||||
if (it.comparison.isNumeric) ParameterType.NUMBER else ParameterType.STRING,
|
||||
it.comparison.value
|
||||
)
|
||||
})
|
||||
return existing
|
||||
}
|
||||
fun addFields(fields: Collection<Field<*>>, existing: MutableCollection<Parameter<*>> = mutableListOf()) =
|
||||
fields.fold(existing) { acc, field -> field.appendParameter(acc) }
|
||||
|
||||
/**
|
||||
* Replace the parameter names in the query with question marks
|
||||
@@ -72,7 +58,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
|
||||
*
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package solutions.bitbadger.documents.query
|
||||
|
||||
import solutions.bitbadger.documents.Configuration
|
||||
import solutions.bitbadger.documents.Dialect
|
||||
import solutions.bitbadger.documents.Field
|
||||
import solutions.bitbadger.documents.FieldFormat
|
||||
import solutions.bitbadger.documents.*
|
||||
|
||||
/**
|
||||
* Functions to create queries to define tables and indexes
|
||||
@@ -76,4 +73,20 @@ object Definition {
|
||||
*/
|
||||
fun ensureKey(tableName: String, dialect: Dialect? = null) =
|
||||
ensureIndexOn(tableName, "key", listOf(Configuration.idField), dialect).replace("INDEX", "UNIQUE INDEX")
|
||||
|
||||
/**
|
||||
* Create a document-wide index on a table (PostgreSQL only)
|
||||
*
|
||||
* @param tableName The name of the table on which the document index should be created
|
||||
* @param indexType The type of index to be created
|
||||
* @return The SQL statement to create an index on JSON documents in the specified table
|
||||
* @throws DocumentException If the database mode is not PostgreSQL
|
||||
*/
|
||||
fun ensureDocumentIndexOn(tableName: String, indexType: DocumentIndex): String {
|
||||
if (Configuration.dialect("create document index query") != Dialect.POSTGRESQL) {
|
||||
throw DocumentException("'Document indexes are only supported on PostgreSQL")
|
||||
}
|
||||
val (_, tbl) = splitSchemaAndTable(tableName)
|
||||
return "CREATE INDEX IF NOT EXISTS idx_${tbl}_document ON $tableName USING GIN (data${indexType.sql})";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user