Initial Development #1
@ -27,7 +27,7 @@ object Configuration {
|
||||
var idStringLength = 16
|
||||
|
||||
/** The derived dialect value from the connection string */
|
||||
private var dialectValue: Dialect? = null
|
||||
internal var dialectValue: Dialect? = null
|
||||
|
||||
/** The connection string for the JDBC connection */
|
||||
var connectionString: String? = null
|
||||
|
@ -1,504 +0,0 @@
|
||||
package solutions.bitbadger.documents
|
||||
|
||||
object Query {
|
||||
|
||||
/**
|
||||
* Combine a query (`SELECT`, `UPDATE`, etc.) and a `WHERE` clause
|
||||
*
|
||||
* @param statement The first part of the statement
|
||||
* @param where The `WHERE` clause for the statement
|
||||
* @return The two parts of the query combined with `WHERE`
|
||||
*/
|
||||
fun statementWhere(statement: String, where: String) =
|
||||
"$statement WHERE $where"
|
||||
|
||||
/**
|
||||
* Functions to create `WHERE` clause fragments
|
||||
*/
|
||||
object Where {
|
||||
|
||||
/**
|
||||
* Create a `WHERE` clause fragment to query by one or more fields
|
||||
*
|
||||
* @param fields The fields to be queried
|
||||
* @param howMatched How the fields should be matched (optional, defaults to `ALL`)
|
||||
* @return A `WHERE` clause fragment to match the given fields
|
||||
*/
|
||||
fun byFields(fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
|
||||
fields.joinToString(" ${(howMatched ?: FieldMatch.ALL).sql} ") { it.toWhere() }
|
||||
|
||||
/**
|
||||
* Create a `WHERE` clause fragment to retrieve a document by its ID
|
||||
*
|
||||
* @param parameterName The parameter name to use for the ID placeholder (optional, defaults to ":id")
|
||||
* @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 ?: "", parameterName)))
|
||||
|
||||
/**
|
||||
* Create a `WHERE` clause fragment to implement a JSON containment query (PostgreSQL only)
|
||||
*
|
||||
* @param parameterName The parameter name to use for the JSON placeholder (optional, defaults to ":criteria")
|
||||
* @return A `WHERE` clause fragment to implement a JSON containment criterion
|
||||
* @throws DocumentException If called against a SQLite database
|
||||
*/
|
||||
fun jsonContains(parameterName: String = ":criteria") =
|
||||
when (Configuration.dialect("create containment WHERE clause")) {
|
||||
Dialect.POSTGRESQL -> "data @> $parameterName"
|
||||
Dialect.SQLITE -> throw DocumentException("JSON containment is not supported")
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a `WHERE` clause fragment to implement a JSON path match query (PostgreSQL only)
|
||||
*
|
||||
* @param parameterName The parameter name to use for the placeholder (optional, defaults to ":path")
|
||||
* @return A `WHERE` clause fragment to implement a JSON path match criterion
|
||||
* @throws DocumentException If called against a SQLite database
|
||||
*/
|
||||
fun jsonPathMatches(parameterName: String = ":path") =
|
||||
when (Configuration.dialect("create JSON path match WHERE clause")) {
|
||||
Dialect.POSTGRESQL -> "jsonb_path_exists(data, $parameterName::jsonpath)"
|
||||
Dialect.SQLITE -> throw DocumentException("JSON path match is not supported")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a query by a document's ID
|
||||
*
|
||||
* @param statement The SQL statement to be run against a document by its ID
|
||||
* @param docId The ID of the document targeted
|
||||
* @returns A query addressing a document by its ID
|
||||
*/
|
||||
fun <TKey> byId(statement: String, docId: TKey) =
|
||||
statementWhere(statement, Where.byId(docId = docId))
|
||||
|
||||
/**
|
||||
* Create a query on JSON fields
|
||||
*
|
||||
* @param statement The SQL statement to be run against matching fields
|
||||
* @param fields The field conditions to be matched
|
||||
* @param howMatched Whether to match any or all of the field conditions (optional; default ALL)
|
||||
* @return A query addressing documents by field matching conditions
|
||||
*/
|
||||
fun byFields(statement: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
|
||||
statementWhere(statement, Where.byFields(fields, howMatched))
|
||||
|
||||
/**
|
||||
* Functions to create queries to define tables and indexes
|
||||
*/
|
||||
object Definition {
|
||||
|
||||
/**
|
||||
* SQL statement to create a document table
|
||||
*
|
||||
* @param tableName The name of the table to create (may include schema)
|
||||
* @param dataType The type of data for the column (`JSON`, `JSONB`, etc.)
|
||||
* @return A query to create a document table
|
||||
*/
|
||||
fun ensureTableFor(tableName: String, dataType: String) =
|
||||
"CREATE TABLE IF NOT EXISTS $tableName (data $dataType NOT NULL)"
|
||||
|
||||
/**
|
||||
* SQL statement to create a document table in the current dialect
|
||||
*
|
||||
* @param tableName The name of the table to create (may include schema)
|
||||
* @return A query to create a document table
|
||||
*/
|
||||
fun ensureTable(tableName: String) =
|
||||
when (Configuration.dialect("create table creation query")) {
|
||||
Dialect.POSTGRESQL -> ensureTableFor(tableName, "JSONB")
|
||||
Dialect.SQLITE -> ensureTableFor(tableName, "TEXT")
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a schema and table name
|
||||
*
|
||||
* @param tableName The name of the table, possibly with a schema
|
||||
* @return A pair with the first item as the schema and the second as the table name
|
||||
*/
|
||||
private fun splitSchemaAndTable(tableName: String) =
|
||||
tableName.split('.').let { if (it.size == 1) Pair("", tableName) else Pair(it[0], it[1]) }
|
||||
|
||||
/**
|
||||
* SQL statement to create an index on one or more fields in a JSON document
|
||||
*
|
||||
* @param tableName The table on which an index should be created (may include schema)
|
||||
* @param indexName The name of the index to be created
|
||||
* @param fields One or more fields to include in the index
|
||||
* @param dialect The SQL dialect to use when creating this index
|
||||
* @return A query to create the field index
|
||||
*/
|
||||
fun ensureIndexOn(tableName: String, indexName: String, fields: Collection<String>, dialect: Dialect): String {
|
||||
val (_, tbl) = splitSchemaAndTable(tableName)
|
||||
val jsonFields = fields.joinToString(", ") {
|
||||
val parts = it.split(' ')
|
||||
val direction = if (parts.size > 1) " ${parts[1]}" else ""
|
||||
"(" + Field.nameToPath(parts[0], dialect, FieldFormat.SQL) + ")$direction"
|
||||
}
|
||||
return "CREATE INDEX IF NOT EXISTS idx_${tbl}_$indexName ON $tableName ($jsonFields)"
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL statement to create a key index for a document table
|
||||
*
|
||||
* @param tableName The table on which a key index should be created (may include schema)
|
||||
* @param dialect The SQL dialect to use when creating this index
|
||||
* @return A query to create the key index
|
||||
*/
|
||||
fun ensureKey(tableName: String, dialect: Dialect) =
|
||||
ensureIndexOn(tableName, "key", listOf(Configuration.idField), dialect).replace("INDEX", "UNIQUE INDEX")
|
||||
}
|
||||
|
||||
/**
|
||||
* Query to insert a document
|
||||
*
|
||||
* @param tableName The table into which to insert (may include schema)
|
||||
* @return A query to insert a document
|
||||
*/
|
||||
fun insert(tableName: String, autoId: AutoId? = null): String {
|
||||
val id = Configuration.idField
|
||||
val values = when (Configuration.dialect("create INSERT statement")) {
|
||||
Dialect.POSTGRESQL -> when (autoId ?: AutoId.DISABLED) {
|
||||
AutoId.DISABLED -> ":data"
|
||||
AutoId.NUMBER -> ":data::jsonb || ('{\"$id\":' || " +
|
||||
"(SELECT COALESCE(MAX((data->>'$id')::numeric), 0) + 1 " +
|
||||
"FROM $tableName) || '}')::jsonb"
|
||||
AutoId.UUID -> ":data::jsonb || '{\"$id\":\"${AutoId.generateUUID()}\"}'"
|
||||
AutoId.RANDOM_STRING -> ":data::jsonb || '{\"$id\":\"${AutoId.generateRandomString()}\"}'"
|
||||
}
|
||||
Dialect.SQLITE -> when (autoId ?: AutoId.DISABLED) {
|
||||
AutoId.DISABLED -> ":data"
|
||||
AutoId.NUMBER -> "json_set(:data, '$.$id', " +
|
||||
"(SELECT coalesce(max(data->>'$id'), 0) + 1 FROM $tableName))"
|
||||
AutoId.UUID -> "json_set(:data, '$.$id', '${AutoId.generateUUID()}')"
|
||||
AutoId.RANDOM_STRING -> "json_set(:data, '$.$id', '${AutoId.generateRandomString()}')"
|
||||
}
|
||||
}
|
||||
return "INSERT INTO $tableName VALUES ($values)"
|
||||
}
|
||||
|
||||
/**
|
||||
* Query to save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")
|
||||
*
|
||||
* @param tableName The table into which to save (may include schema)
|
||||
* @return A query to save a document
|
||||
*/
|
||||
fun save(tableName: String) =
|
||||
insert(tableName, AutoId.DISABLED) +
|
||||
" ON CONFLICT ((data->>'${Configuration.idField}')) DO UPDATE SET data = EXCLUDED.data"
|
||||
|
||||
/**
|
||||
* Functions to count documents
|
||||
*/
|
||||
object Count {
|
||||
|
||||
/**
|
||||
* Query to count all documents in a table
|
||||
*
|
||||
* @param tableName The table in which to count documents (may include schema)
|
||||
* @return A query to count documents
|
||||
*/
|
||||
fun all(tableName: String) =
|
||||
"SELECT COUNT(*) AS it FROM $tableName"
|
||||
|
||||
/**
|
||||
* Query to count documents matching the given fields
|
||||
*
|
||||
* @param tableName The table in which to count documents (may include schema)
|
||||
* @param fields The field comparisons for the count
|
||||
* @param howMatched How fields should be compared (optional, defaults to ALL)
|
||||
* @return A query to count documents matching the given fields
|
||||
*/
|
||||
fun byFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
|
||||
statementWhere(all(tableName), Where.byFields(fields, howMatched))
|
||||
|
||||
/**
|
||||
* Query to count documents via JSON containment (PostgreSQL only)
|
||||
*
|
||||
* @param tableName The table in which to count documents (may include schema)
|
||||
* @return A query to count documents via JSON containment
|
||||
*/
|
||||
fun byContains(tableName: String) =
|
||||
statementWhere(all(tableName), Where.jsonContains())
|
||||
|
||||
/**
|
||||
* Query to count documents via a JSON path match (PostgreSQL only)
|
||||
*
|
||||
* @param tableName The table in which to count documents (may include schema)
|
||||
* @return A query to count documents via a JSON path match
|
||||
*/
|
||||
fun byJsonPath(tableName: String) =
|
||||
statementWhere(all(tableName), Where.jsonPathMatches())
|
||||
}
|
||||
|
||||
/**
|
||||
* Functions to check for document existence
|
||||
*/
|
||||
object Exists {
|
||||
|
||||
/**
|
||||
* Query to check for document existence in a table
|
||||
*
|
||||
* @param tableName The table in which existence should be checked (may include schema)
|
||||
* @param where The `WHERE` clause with the existence criteria
|
||||
* @return A query to check document existence
|
||||
*/
|
||||
private fun exists(tableName: String, where: String) =
|
||||
"SELECT EXISTS (SELECT 1 FROM $tableName WHERE $where) AS it"
|
||||
|
||||
/**
|
||||
* Query to check for document existence by ID
|
||||
*
|
||||
* @param tableName The table in which existence should be checked (may include schema)
|
||||
* @param docId The ID of the document (optional, used for type checking)
|
||||
* @return A query to determine document existence by ID
|
||||
*/
|
||||
fun <TKey> byId(tableName: String, docId: TKey? = null) =
|
||||
exists(tableName, Where.byId(docId = docId))
|
||||
|
||||
/**
|
||||
* Query to check for document existence matching the given fields
|
||||
*
|
||||
* @param tableName The table in which existence should be checked (may include schema)
|
||||
* @param fields The field comparisons for the existence check
|
||||
* @param howMatched How fields should be compared (optional, defaults to ALL)
|
||||
* @return A query to determine document existence for the given fields
|
||||
*/
|
||||
fun byFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
|
||||
exists(tableName, Where.byFields(fields, howMatched))
|
||||
|
||||
/**
|
||||
* Query to check for document existence via JSON containment (PostgreSQL only)
|
||||
*
|
||||
* @param tableName The table in which existence should be checked (may include schema)
|
||||
* @return A query to determine document existence via JSON containment
|
||||
*/
|
||||
fun byContains(tableName: String) =
|
||||
exists(tableName, Where.jsonContains())
|
||||
|
||||
/**
|
||||
* Query to check for document existence via a JSON path match (PostgreSQL only)
|
||||
*
|
||||
* @param tableName The table in which existence should be checked (may include schema)
|
||||
* @return A query to determine document existence via a JSON path match
|
||||
*/
|
||||
fun byJsonPath(tableName: String) =
|
||||
exists(tableName, Where.jsonPathMatches())
|
||||
}
|
||||
|
||||
/**
|
||||
* Functions to retrieve documents
|
||||
*/
|
||||
object Find {
|
||||
|
||||
/**
|
||||
* Query to retrieve all documents from a table
|
||||
*
|
||||
* @param tableName The table from which documents should be retrieved (may include schema)
|
||||
* @return A query to retrieve documents
|
||||
*/
|
||||
fun all(tableName: String) =
|
||||
"SELECT data FROM $tableName"
|
||||
|
||||
/**
|
||||
* Query to retrieve a document by its ID
|
||||
*
|
||||
* @param tableName The table from which documents should be retrieved (may include schema)
|
||||
* @param docId The ID of the document (optional, used for type checking)
|
||||
* @return A query to retrieve a document by its ID
|
||||
*/
|
||||
fun <TKey> byId(tableName: String, docId: TKey?) =
|
||||
statementWhere(all(tableName), Where.byId(docId = docId))
|
||||
|
||||
/**
|
||||
* Query to retrieve documents matching the given fields
|
||||
*
|
||||
* @param tableName The table from which documents should be retrieved (may include schema)
|
||||
* @param fields The field comparisons for matching documents to retrieve
|
||||
* @param howMatched How fields should be compared (optional, defaults to ALL)
|
||||
* @return A query to retrieve documents matching the given fields
|
||||
*/
|
||||
fun byFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch?) =
|
||||
statementWhere(all(tableName), Where.byFields(fields, howMatched))
|
||||
|
||||
/**
|
||||
* Query to retrieve documents via JSON containment (PostgreSQL only)
|
||||
*
|
||||
* @param tableName The table from which documents should be retrieved (may include schema)
|
||||
* @return A query to retrieve documents via JSON containment
|
||||
*/
|
||||
fun byContains(tableName: String) =
|
||||
statementWhere(all(tableName), Where.jsonContains())
|
||||
|
||||
/**
|
||||
* Query to retrieve documents via a JSON path match (PostgreSQL only)
|
||||
*
|
||||
* @param tableName The table from which documents should be retrieved (may include schema)
|
||||
* @return A query to retrieve documents via a JSON path match
|
||||
*/
|
||||
fun byJsonPath(tableName: String) =
|
||||
statementWhere(all(tableName), Where.jsonPathMatches())
|
||||
}
|
||||
|
||||
/**
|
||||
* Query to update (replace) a document (this query has no `WHERE` clause)
|
||||
*
|
||||
* @param tableName The table in which documents should be replaced (may include schema)
|
||||
* @return A query to update documents
|
||||
*/
|
||||
fun update(tableName: String) =
|
||||
"UPDATE $tableName SET data = :data"
|
||||
|
||||
/**
|
||||
* Functions to create queries to patch (partially update) JSON documents
|
||||
*/
|
||||
object Patch {
|
||||
|
||||
/**
|
||||
* Create an `UPDATE` statement to patch documents
|
||||
*
|
||||
* @param tableName The table to be updated
|
||||
* @param where The `WHERE` clause for the query
|
||||
* @return A query to patch documents
|
||||
*/
|
||||
private fun patch(tableName: String, where: String): String {
|
||||
val setValue = when (Configuration.dialect("create patch query")) {
|
||||
Dialect.POSTGRESQL -> "data || :data"
|
||||
Dialect.SQLITE -> "json_patch(data, json(:data))"
|
||||
}
|
||||
return statementWhere("UPDATE $tableName SET data = $setValue", where)
|
||||
}
|
||||
|
||||
/**
|
||||
* A query to patch (partially update) a JSON document by its ID
|
||||
*
|
||||
* @param tableName The name of the table where the document is stored
|
||||
* @param docId The ID of the document to be updated (optional, used for type checking)
|
||||
* @return A query to patch a JSON document by its ID
|
||||
*/
|
||||
fun <TKey> byId(tableName: String, docId: TKey? = null) =
|
||||
patch(tableName, Where.byId(docId = docId))
|
||||
|
||||
/**
|
||||
* A query to patch (partially update) a JSON document using field match criteria
|
||||
*
|
||||
* @param tableName The name of the table where the documents are stored
|
||||
* @param fields The field criteria
|
||||
* @param howMatched How the fields should be matched (optional, defaults to `ALL`)
|
||||
* @return A query to patch JSON documents by field match criteria
|
||||
*/
|
||||
fun byFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
|
||||
patch(tableName, Where.byFields(fields, howMatched))
|
||||
|
||||
/**
|
||||
* A query to patch (partially update) a JSON document by JSON containment (PostgreSQL only)
|
||||
*
|
||||
* @param tableName The name of the table where the document is stored
|
||||
* @return A query to patch JSON documents by JSON containment
|
||||
*/
|
||||
fun <TKey> byContains(tableName: String) =
|
||||
patch(tableName, Where.jsonContains())
|
||||
|
||||
/**
|
||||
* A query to patch (partially update) a JSON document by JSON path match (PostgreSQL only)
|
||||
*
|
||||
* @param tableName The name of the table where the document is stored
|
||||
* @return A query to patch JSON documents by JSON path match
|
||||
*/
|
||||
fun <TKey> byJsonPath(tableName: String) =
|
||||
patch(tableName, Where.jsonPathMatches())
|
||||
}
|
||||
|
||||
/**
|
||||
* Functions to delete documents
|
||||
*/
|
||||
object Delete {
|
||||
|
||||
/**
|
||||
* Query to delete documents from a table
|
||||
*
|
||||
* @param tableName The table in which documents should be deleted (may include schema)
|
||||
* @param where The WHERE clause for the delete statement
|
||||
* @return A query to delete documents
|
||||
*/
|
||||
private fun delete(tableName: String, where: String) =
|
||||
statementWhere("DELETE FROM $tableName", where)
|
||||
|
||||
/**
|
||||
* Query to delete a document by its ID
|
||||
*
|
||||
* @param tableName The table from which documents should be deleted (may include schema)
|
||||
* @param docId The ID of the document (optional, used for type checking)
|
||||
* @return A query to delete a document by its ID
|
||||
*/
|
||||
fun <TKey> byId(tableName: String, docId: TKey?) =
|
||||
delete(tableName, Where.byId(docId = docId))
|
||||
|
||||
/**
|
||||
* Query to delete documents matching the given fields
|
||||
*
|
||||
* @param tableName The table from which documents should be deleted (may include schema)
|
||||
* @param fields The field comparisons for documents to be deleted
|
||||
* @param howMatched How fields should be compared (optional, defaults to ALL)
|
||||
* @return A query to delete documents matching for the given fields
|
||||
*/
|
||||
fun byFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch?) =
|
||||
delete(tableName, Where.byFields(fields, howMatched))
|
||||
|
||||
/**
|
||||
* Query to delete documents via JSON containment (PostgreSQL only)
|
||||
*
|
||||
* @param tableName The table from which documents should be deleted (may include schema)
|
||||
* @return A query to delete documents via JSON containment
|
||||
*/
|
||||
fun byContains(tableName: String) =
|
||||
delete(tableName, Where.jsonContains())
|
||||
|
||||
/**
|
||||
* Query to delete documents via a JSON path match (PostgreSQL only)
|
||||
*
|
||||
* @param tableName The table from which documents should be deleted (may include schema)
|
||||
* @return A query to delete documents via a JSON path match
|
||||
*/
|
||||
fun byJsonPath(tableName: String) =
|
||||
delete(tableName, Where.jsonPathMatches())
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an `ORDER BY` clause for the given fields
|
||||
*
|
||||
* @param fields One or more fields by which to order
|
||||
* @param dialect The SQL dialect for the generated clause
|
||||
* @return An `ORDER BY` clause for the given fields
|
||||
*/
|
||||
fun orderBy(fields: Collection<Field<*>>, dialect: Dialect): String {
|
||||
if (fields.isEmpty()) return ""
|
||||
val orderFields = fields.joinToString(", ") {
|
||||
val (field, direction) =
|
||||
if (it.name.indexOf(' ') > -1) {
|
||||
val parts = it.name.split(' ')
|
||||
Pair(Field.named(parts[0]), " " + parts.drop(1).joinToString(" "))
|
||||
} else {
|
||||
Pair<Field<*>, String?>(it, null)
|
||||
}
|
||||
val path = when {
|
||||
field.name.startsWith("n:") -> Field.named(field.name.substring(2)).let { fld ->
|
||||
when (dialect) {
|
||||
Dialect.POSTGRESQL -> "(${fld.path(dialect)})::numeric"
|
||||
Dialect.SQLITE -> fld.path(dialect)
|
||||
}
|
||||
}
|
||||
field.name.startsWith("i:") -> Field.named(field.name.substring(2)).path(dialect).let { p ->
|
||||
when (dialect) {
|
||||
Dialect.POSTGRESQL -> "LOWER($p)"
|
||||
Dialect.SQLITE -> "$p COLLATE NOCASE"
|
||||
}
|
||||
}
|
||||
else -> field.path(dialect)
|
||||
}
|
||||
"$path${direction ?: ""}"
|
||||
}
|
||||
return " ORDER BY $orderFields"
|
||||
}
|
||||
}
|
49
src/main/kotlin/query/Count.kt
Normal file
49
src/main/kotlin/query/Count.kt
Normal file
@ -0,0 +1,49 @@
|
||||
package solutions.bitbadger.documents.query
|
||||
|
||||
import solutions.bitbadger.documents.Field
|
||||
import solutions.bitbadger.documents.FieldMatch
|
||||
import solutions.bitbadger.documents.query.byFields as byFieldsBase;
|
||||
|
||||
/**
|
||||
* Functions to count documents
|
||||
*/
|
||||
object Count {
|
||||
|
||||
/**
|
||||
* Query to count all documents in a table
|
||||
*
|
||||
* @param tableName The table in which to count documents (may include schema)
|
||||
* @return A query to count documents
|
||||
*/
|
||||
fun all(tableName: String) =
|
||||
"SELECT COUNT(*) AS it FROM $tableName"
|
||||
|
||||
/**
|
||||
* Query to count documents matching the given fields
|
||||
*
|
||||
* @param tableName The table in which to count documents (may include schema)
|
||||
* @param fields The field comparisons for the count
|
||||
* @param howMatched How fields should be compared (optional, defaults to ALL)
|
||||
* @return A query to count documents matching the given fields
|
||||
*/
|
||||
fun byFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
|
||||
byFieldsBase(all(tableName), fields, howMatched)
|
||||
|
||||
/**
|
||||
* Query to count documents via JSON containment (PostgreSQL only)
|
||||
*
|
||||
* @param tableName The table in which to count documents (may include schema)
|
||||
* @return A query to count documents via JSON containment
|
||||
*/
|
||||
fun byContains(tableName: String) =
|
||||
statementWhere(all(tableName), Where.jsonContains())
|
||||
|
||||
/**
|
||||
* Query to count documents via a JSON path match (PostgreSQL only)
|
||||
*
|
||||
* @param tableName The table in which to count documents (may include schema)
|
||||
* @return A query to count documents via a JSON path match
|
||||
*/
|
||||
fun byJsonPath(tableName: String) =
|
||||
statementWhere(all(tableName), Where.jsonPathMatches())
|
||||
}
|
72
src/main/kotlin/query/Definition.kt
Normal file
72
src/main/kotlin/query/Definition.kt
Normal file
@ -0,0 +1,72 @@
|
||||
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
|
||||
|
||||
/**
|
||||
* Functions to create queries to define tables and indexes
|
||||
*/
|
||||
object Definition {
|
||||
|
||||
/**
|
||||
* SQL statement to create a document table
|
||||
*
|
||||
* @param tableName The name of the table to create (may include schema)
|
||||
* @param dataType The type of data for the column (`JSON`, `JSONB`, etc.)
|
||||
* @return A query to create a document table
|
||||
*/
|
||||
fun ensureTableFor(tableName: String, dataType: String) =
|
||||
"CREATE TABLE IF NOT EXISTS $tableName (data $dataType NOT NULL)"
|
||||
|
||||
/**
|
||||
* SQL statement to create a document table in the current dialect
|
||||
*
|
||||
* @param tableName The name of the table to create (may include schema)
|
||||
* @return A query to create a document table
|
||||
*/
|
||||
fun ensureTable(tableName: String) =
|
||||
when (Configuration.dialect("create table creation query")) {
|
||||
Dialect.POSTGRESQL -> ensureTableFor(tableName, "JSONB")
|
||||
Dialect.SQLITE -> ensureTableFor(tableName, "TEXT")
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a schema and table name
|
||||
*
|
||||
* @param tableName The name of the table, possibly with a schema
|
||||
* @return A pair with the first item as the schema and the second as the table name
|
||||
*/
|
||||
private fun splitSchemaAndTable(tableName: String) =
|
||||
tableName.split('.').let { if (it.size == 1) Pair("", tableName) else Pair(it[0], it[1]) }
|
||||
|
||||
/**
|
||||
* SQL statement to create an index on one or more fields in a JSON document
|
||||
*
|
||||
* @param tableName The table on which an index should be created (may include schema)
|
||||
* @param indexName The name of the index to be created
|
||||
* @param fields One or more fields to include in the index
|
||||
* @param dialect The SQL dialect to use when creating this index
|
||||
* @return A query to create the field index
|
||||
*/
|
||||
fun ensureIndexOn(tableName: String, indexName: String, fields: Collection<String>, dialect: Dialect): String {
|
||||
val (_, tbl) = splitSchemaAndTable(tableName)
|
||||
val jsonFields = fields.joinToString(", ") {
|
||||
val parts = it.split(' ')
|
||||
val direction = if (parts.size > 1) " ${parts[1]}" else ""
|
||||
"(" + Field.nameToPath(parts[0], dialect, FieldFormat.SQL) + ")$direction"
|
||||
}
|
||||
return "CREATE INDEX IF NOT EXISTS idx_${tbl}_$indexName ON $tableName ($jsonFields)"
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL statement to create a key index for a document table
|
||||
*
|
||||
* @param tableName The table on which a key index should be created (may include schema)
|
||||
* @param dialect The SQL dialect to use when creating this index
|
||||
* @return A query to create the key index
|
||||
*/
|
||||
fun ensureKey(tableName: String, dialect: Dialect) =
|
||||
ensureIndexOn(tableName, "key", listOf(Configuration.idField), dialect).replace("INDEX", "UNIQUE INDEX")
|
||||
}
|
61
src/main/kotlin/query/Delete.kt
Normal file
61
src/main/kotlin/query/Delete.kt
Normal file
@ -0,0 +1,61 @@
|
||||
package solutions.bitbadger.documents.query
|
||||
|
||||
import solutions.bitbadger.documents.Field
|
||||
import solutions.bitbadger.documents.FieldMatch
|
||||
import solutions.bitbadger.documents.query.byFields as byFieldsBase
|
||||
import solutions.bitbadger.documents.query.byId as byIdBase
|
||||
|
||||
/**
|
||||
* Functions to delete documents
|
||||
*/
|
||||
object Delete {
|
||||
|
||||
/**
|
||||
* Query to delete documents from a table
|
||||
*
|
||||
* @param tableName The table in which documents should be deleted (may include schema)
|
||||
* @param where The WHERE clause for the delete statement
|
||||
* @return A query to delete documents
|
||||
*/
|
||||
private fun delete(tableName: String) =
|
||||
"DELETE FROM $tableName"
|
||||
|
||||
/**
|
||||
* Query to delete a document by its ID
|
||||
*
|
||||
* @param tableName The table from which documents should be deleted (may include schema)
|
||||
* @param docId The ID of the document (optional, used for type checking)
|
||||
* @return A query to delete a document by its ID
|
||||
*/
|
||||
fun <TKey> byId(tableName: String, docId: TKey? = null) =
|
||||
byIdBase(delete(tableName), docId)
|
||||
|
||||
/**
|
||||
* Query to delete documents matching the given fields
|
||||
*
|
||||
* @param tableName The table from which documents should be deleted (may include schema)
|
||||
* @param fields The field comparisons for documents to be deleted
|
||||
* @param howMatched How fields should be compared (optional, defaults to ALL)
|
||||
* @return A query to delete documents matching for the given fields
|
||||
*/
|
||||
fun byFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
|
||||
byFieldsBase(delete(tableName), fields, howMatched)
|
||||
|
||||
/**
|
||||
* Query to delete documents via JSON containment (PostgreSQL only)
|
||||
*
|
||||
* @param tableName The table from which documents should be deleted (may include schema)
|
||||
* @return A query to delete documents via JSON containment
|
||||
*/
|
||||
fun byContains(tableName: String) =
|
||||
statementWhere(delete(tableName), Where.jsonContains())
|
||||
|
||||
/**
|
||||
* Query to delete documents via a JSON path match (PostgreSQL only)
|
||||
*
|
||||
* @param tableName The table from which documents should be deleted (may include schema)
|
||||
* @return A query to delete documents via a JSON path match
|
||||
*/
|
||||
fun byJsonPath(tableName: String) =
|
||||
statementWhere(delete(tableName), Where.jsonPathMatches())
|
||||
}
|
58
src/main/kotlin/query/Document.kt
Normal file
58
src/main/kotlin/query/Document.kt
Normal file
@ -0,0 +1,58 @@
|
||||
package solutions.bitbadger.documents.query
|
||||
|
||||
import solutions.bitbadger.documents.AutoId
|
||||
import solutions.bitbadger.documents.Configuration
|
||||
import solutions.bitbadger.documents.Dialect
|
||||
|
||||
/**
|
||||
* Functions for document-level operations
|
||||
*/
|
||||
object Document {
|
||||
|
||||
/**
|
||||
* Query to insert a document
|
||||
*
|
||||
* @param tableName The table into which to insert (may include schema)
|
||||
* @return A query to insert a document
|
||||
*/
|
||||
fun insert(tableName: String, autoId: AutoId? = null): String {
|
||||
val id = Configuration.idField
|
||||
val values = when (Configuration.dialect("create INSERT statement")) {
|
||||
Dialect.POSTGRESQL -> when (autoId ?: AutoId.DISABLED) {
|
||||
AutoId.DISABLED -> ":data"
|
||||
AutoId.NUMBER -> ":data::jsonb || ('{\"$id\":' || " +
|
||||
"(SELECT COALESCE(MAX((data->>'$id')::numeric), 0) + 1 " +
|
||||
"FROM $tableName) || '}')::jsonb"
|
||||
AutoId.UUID -> ":data::jsonb || '{\"$id\":\"${AutoId.generateUUID()}\"}'"
|
||||
AutoId.RANDOM_STRING -> ":data::jsonb || '{\"$id\":\"${AutoId.generateRandomString()}\"}'"
|
||||
}
|
||||
Dialect.SQLITE -> when (autoId ?: AutoId.DISABLED) {
|
||||
AutoId.DISABLED -> ":data"
|
||||
AutoId.NUMBER -> "json_set(:data, '$.$id', " +
|
||||
"(SELECT coalesce(max(data->>'$id'), 0) + 1 FROM $tableName))"
|
||||
AutoId.UUID -> "json_set(:data, '$.$id', '${AutoId.generateUUID()}')"
|
||||
AutoId.RANDOM_STRING -> "json_set(:data, '$.$id', '${AutoId.generateRandomString()}')"
|
||||
}
|
||||
}
|
||||
return "INSERT INTO $tableName VALUES ($values)"
|
||||
}
|
||||
|
||||
/**
|
||||
* Query to save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")
|
||||
*
|
||||
* @param tableName The table into which to save (may include schema)
|
||||
* @return A query to save a document
|
||||
*/
|
||||
fun save(tableName: String) =
|
||||
insert(tableName, AutoId.DISABLED) +
|
||||
" ON CONFLICT ((data->>'${Configuration.idField}')) DO UPDATE SET data = EXCLUDED.data"
|
||||
|
||||
/**
|
||||
* Query to update (replace) a document (this query has no `WHERE` clause)
|
||||
*
|
||||
* @param tableName The table in which documents should be replaced (may include schema)
|
||||
* @return A query to update documents
|
||||
*/
|
||||
fun update(tableName: String) =
|
||||
"UPDATE $tableName SET data = :data"
|
||||
}
|
59
src/main/kotlin/query/Exists.kt
Normal file
59
src/main/kotlin/query/Exists.kt
Normal file
@ -0,0 +1,59 @@
|
||||
package solutions.bitbadger.documents.query
|
||||
|
||||
import solutions.bitbadger.documents.Field
|
||||
import solutions.bitbadger.documents.FieldMatch
|
||||
|
||||
/**
|
||||
* Functions to check for document existence
|
||||
*/
|
||||
object Exists {
|
||||
|
||||
/**
|
||||
* Query to check for document existence in a table
|
||||
*
|
||||
* @param tableName The table in which existence should be checked (may include schema)
|
||||
* @param where The `WHERE` clause with the existence criteria
|
||||
* @return A query to check document existence
|
||||
*/
|
||||
private fun exists(tableName: String, where: String) =
|
||||
"SELECT EXISTS (SELECT 1 FROM $tableName WHERE $where) AS it"
|
||||
|
||||
/**
|
||||
* Query to check for document existence by ID
|
||||
*
|
||||
* @param tableName The table in which existence should be checked (may include schema)
|
||||
* @param docId The ID of the document (optional, used for type checking)
|
||||
* @return A query to determine document existence by ID
|
||||
*/
|
||||
fun <TKey> byId(tableName: String, docId: TKey? = null) =
|
||||
exists(tableName, Where.byId(docId = docId))
|
||||
|
||||
/**
|
||||
* Query to check for document existence matching the given fields
|
||||
*
|
||||
* @param tableName The table in which existence should be checked (may include schema)
|
||||
* @param fields The field comparisons for the existence check
|
||||
* @param howMatched How fields should be compared (optional, defaults to ALL)
|
||||
* @return A query to determine document existence for the given fields
|
||||
*/
|
||||
fun byFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
|
||||
exists(tableName, Where.byFields(fields, howMatched))
|
||||
|
||||
/**
|
||||
* Query to check for document existence via JSON containment (PostgreSQL only)
|
||||
*
|
||||
* @param tableName The table in which existence should be checked (may include schema)
|
||||
* @return A query to determine document existence via JSON containment
|
||||
*/
|
||||
fun byContains(tableName: String) =
|
||||
exists(tableName, Where.jsonContains())
|
||||
|
||||
/**
|
||||
* Query to check for document existence via a JSON path match (PostgreSQL only)
|
||||
*
|
||||
* @param tableName The table in which existence should be checked (may include schema)
|
||||
* @return A query to determine document existence via a JSON path match
|
||||
*/
|
||||
fun byJsonPath(tableName: String) =
|
||||
exists(tableName, Where.jsonPathMatches())
|
||||
}
|
60
src/main/kotlin/query/Find.kt
Normal file
60
src/main/kotlin/query/Find.kt
Normal file
@ -0,0 +1,60 @@
|
||||
package solutions.bitbadger.documents.query
|
||||
|
||||
import solutions.bitbadger.documents.Field
|
||||
import solutions.bitbadger.documents.FieldMatch
|
||||
import solutions.bitbadger.documents.query.byId as byIdBase
|
||||
import solutions.bitbadger.documents.query.byFields as byFieldsBase
|
||||
|
||||
/**
|
||||
* Functions to retrieve documents
|
||||
*/
|
||||
object Find {
|
||||
|
||||
/**
|
||||
* Query to retrieve all documents from a table
|
||||
*
|
||||
* @param tableName The table from which documents should be retrieved (may include schema)
|
||||
* @return A query to retrieve documents
|
||||
*/
|
||||
fun all(tableName: String) =
|
||||
"SELECT data FROM $tableName"
|
||||
|
||||
/**
|
||||
* Query to retrieve a document by its ID
|
||||
*
|
||||
* @param tableName The table from which documents should be retrieved (may include schema)
|
||||
* @param docId The ID of the document (optional, used for type checking)
|
||||
* @return A query to retrieve a document by its ID
|
||||
*/
|
||||
fun <TKey> byId(tableName: String, docId: TKey? = null) =
|
||||
byIdBase(all(tableName), docId)
|
||||
|
||||
/**
|
||||
* Query to retrieve documents matching the given fields
|
||||
*
|
||||
* @param tableName The table from which documents should be retrieved (may include schema)
|
||||
* @param fields The field comparisons for matching documents to retrieve
|
||||
* @param howMatched How fields should be compared (optional, defaults to ALL)
|
||||
* @return A query to retrieve documents matching the given fields
|
||||
*/
|
||||
fun byFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
|
||||
byFieldsBase(all(tableName), fields, howMatched)
|
||||
|
||||
/**
|
||||
* Query to retrieve documents via JSON containment (PostgreSQL only)
|
||||
*
|
||||
* @param tableName The table from which documents should be retrieved (may include schema)
|
||||
* @return A query to retrieve documents via JSON containment
|
||||
*/
|
||||
fun byContains(tableName: String) =
|
||||
statementWhere(all(tableName), Where.jsonContains())
|
||||
|
||||
/**
|
||||
* Query to retrieve documents via a JSON path match (PostgreSQL only)
|
||||
*
|
||||
* @param tableName The table from which documents should be retrieved (may include schema)
|
||||
* @return A query to retrieve documents via a JSON path match
|
||||
*/
|
||||
fun byJsonPath(tableName: String) =
|
||||
statementWhere(all(tableName), Where.jsonPathMatches())
|
||||
}
|
65
src/main/kotlin/query/Patch.kt
Normal file
65
src/main/kotlin/query/Patch.kt
Normal file
@ -0,0 +1,65 @@
|
||||
package solutions.bitbadger.documents.query
|
||||
|
||||
import solutions.bitbadger.documents.Configuration
|
||||
import solutions.bitbadger.documents.Dialect
|
||||
import solutions.bitbadger.documents.Field
|
||||
import solutions.bitbadger.documents.FieldMatch
|
||||
import solutions.bitbadger.documents.query.byFields as byFieldsBase
|
||||
import solutions.bitbadger.documents.query.byId as byIdBase
|
||||
|
||||
/**
|
||||
* Functions to create queries to patch (partially update) JSON documents
|
||||
*/
|
||||
object Patch {
|
||||
|
||||
/**
|
||||
* Create an `UPDATE` statement to patch documents
|
||||
*
|
||||
* @param tableName The table to be updated
|
||||
* @return A query to patch documents
|
||||
*/
|
||||
private fun patch(tableName: String) =
|
||||
when (Configuration.dialect("create patch query")) {
|
||||
Dialect.POSTGRESQL -> "data || :data"
|
||||
Dialect.SQLITE -> "json_patch(data, json(:data))"
|
||||
}.let { "UPDATE $tableName SET data = $it" }
|
||||
|
||||
/**
|
||||
* A query to patch (partially update) a JSON document by its ID
|
||||
*
|
||||
* @param tableName The name of the table where the document is stored
|
||||
* @param docId The ID of the document to be updated (optional, used for type checking)
|
||||
* @return A query to patch a JSON document by its ID
|
||||
*/
|
||||
fun <TKey> byId(tableName: String, docId: TKey? = null) =
|
||||
byIdBase(patch(tableName), docId)
|
||||
|
||||
/**
|
||||
* A query to patch (partially update) a JSON document using field match criteria
|
||||
*
|
||||
* @param tableName The name of the table where the documents are stored
|
||||
* @param fields The field criteria
|
||||
* @param howMatched How the fields should be matched (optional, defaults to `ALL`)
|
||||
* @return A query to patch JSON documents by field match criteria
|
||||
*/
|
||||
fun byFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
|
||||
byFieldsBase(patch(tableName), fields, howMatched)
|
||||
|
||||
/**
|
||||
* A query to patch (partially update) a JSON document by JSON containment (PostgreSQL only)
|
||||
*
|
||||
* @param tableName The name of the table where the document is stored
|
||||
* @return A query to patch JSON documents by JSON containment
|
||||
*/
|
||||
fun <TKey> byContains(tableName: String) =
|
||||
statementWhere(patch(tableName), Where.jsonContains())
|
||||
|
||||
/**
|
||||
* A query to patch (partially update) a JSON document by JSON path match (PostgreSQL only)
|
||||
*
|
||||
* @param tableName The name of the table where the document is stored
|
||||
* @return A query to patch JSON documents by JSON path match
|
||||
*/
|
||||
fun <TKey> byJsonPath(tableName: String) =
|
||||
statementWhere(patch(tableName), Where.jsonPathMatches())
|
||||
}
|
75
src/main/kotlin/query/Query.kt
Normal file
75
src/main/kotlin/query/Query.kt
Normal file
@ -0,0 +1,75 @@
|
||||
package solutions.bitbadger.documents.query
|
||||
|
||||
import solutions.bitbadger.documents.Dialect
|
||||
import solutions.bitbadger.documents.Field
|
||||
import solutions.bitbadger.documents.FieldMatch
|
||||
|
||||
// ~~~ TOP-LEVEL FUNCTIONS FOR THE QUERY PACKAGE ~~~
|
||||
|
||||
/**
|
||||
* Combine a query (`SELECT`, `UPDATE`, etc.) and a `WHERE` clause
|
||||
*
|
||||
* @param statement The first part of the statement
|
||||
* @param where The `WHERE` clause for the statement
|
||||
* @return The two parts of the query combined with `WHERE`
|
||||
*/
|
||||
fun statementWhere(statement: String, where: String) =
|
||||
"$statement WHERE $where"
|
||||
|
||||
/**
|
||||
* Create a query by a document's ID
|
||||
*
|
||||
* @param statement The SQL statement to be run against a document by its ID
|
||||
* @param docId The ID of the document targeted
|
||||
* @returns A query addressing a document by its ID
|
||||
*/
|
||||
fun <TKey> byId(statement: String, docId: TKey) =
|
||||
statementWhere(statement, Where.byId(docId = docId))
|
||||
|
||||
/**
|
||||
* Create a query on JSON fields
|
||||
*
|
||||
* @param statement The SQL statement to be run against matching fields
|
||||
* @param fields The field conditions to be matched
|
||||
* @param howMatched Whether to match any or all of the field conditions (optional; default ALL)
|
||||
* @return A query addressing documents by field matching conditions
|
||||
*/
|
||||
fun byFields(statement: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
|
||||
statementWhere(statement, Where.byFields(fields, howMatched))
|
||||
|
||||
/**
|
||||
* Create an `ORDER BY` clause for the given fields
|
||||
*
|
||||
* @param fields One or more fields by which to order
|
||||
* @param dialect The SQL dialect for the generated clause
|
||||
* @return An `ORDER BY` clause for the given fields
|
||||
*/
|
||||
fun orderBy(fields: Collection<Field<*>>, dialect: Dialect): String {
|
||||
if (fields.isEmpty()) return ""
|
||||
val orderFields = fields.joinToString(", ") {
|
||||
val (field, direction) =
|
||||
if (it.name.indexOf(' ') > -1) {
|
||||
val parts = it.name.split(' ')
|
||||
Pair(Field.named(parts[0]), " " + parts.drop(1).joinToString(" "))
|
||||
} else {
|
||||
Pair<Field<*>, String?>(it, null)
|
||||
}
|
||||
val path = when {
|
||||
field.name.startsWith("n:") -> Field.named(field.name.substring(2)).let { fld ->
|
||||
when (dialect) {
|
||||
Dialect.POSTGRESQL -> "(${fld.path(dialect)})::numeric"
|
||||
Dialect.SQLITE -> fld.path(dialect)
|
||||
}
|
||||
}
|
||||
field.name.startsWith("i:") -> Field.named(field.name.substring(2)).path(dialect).let { p ->
|
||||
when (dialect) {
|
||||
Dialect.POSTGRESQL -> "LOWER($p)"
|
||||
Dialect.SQLITE -> "$p COLLATE NOCASE"
|
||||
}
|
||||
}
|
||||
else -> field.path(dialect)
|
||||
}
|
||||
"$path${direction ?: ""}"
|
||||
}
|
||||
return " ORDER BY $orderFields"
|
||||
}
|
54
src/main/kotlin/query/Where.kt
Normal file
54
src/main/kotlin/query/Where.kt
Normal file
@ -0,0 +1,54 @@
|
||||
package solutions.bitbadger.documents.query
|
||||
|
||||
import solutions.bitbadger.documents.*
|
||||
|
||||
/**
|
||||
* Functions to create `WHERE` clause fragments
|
||||
*/
|
||||
object Where {
|
||||
|
||||
/**
|
||||
* Create a `WHERE` clause fragment to query by one or more fields
|
||||
*
|
||||
* @param fields The fields to be queried
|
||||
* @param howMatched How the fields should be matched (optional, defaults to `ALL`)
|
||||
* @return A `WHERE` clause fragment to match the given fields
|
||||
*/
|
||||
fun byFields(fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
|
||||
fields.joinToString(" ${(howMatched ?: FieldMatch.ALL).sql} ") { it.toWhere() }
|
||||
|
||||
/**
|
||||
* Create a `WHERE` clause fragment to retrieve a document by its ID
|
||||
*
|
||||
* @param parameterName The parameter name to use for the ID placeholder (optional, defaults to ":id")
|
||||
* @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 ?: "", parameterName)))
|
||||
|
||||
/**
|
||||
* Create a `WHERE` clause fragment to implement a JSON containment query (PostgreSQL only)
|
||||
*
|
||||
* @param parameterName The parameter name to use for the JSON placeholder (optional, defaults to ":criteria")
|
||||
* @return A `WHERE` clause fragment to implement a JSON containment criterion
|
||||
* @throws DocumentException If called against a SQLite database
|
||||
*/
|
||||
fun jsonContains(parameterName: String = ":criteria") =
|
||||
when (Configuration.dialect("create containment WHERE clause")) {
|
||||
Dialect.POSTGRESQL -> "data @> $parameterName"
|
||||
Dialect.SQLITE -> throw DocumentException("JSON containment is not supported")
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a `WHERE` clause fragment to implement a JSON path match query (PostgreSQL only)
|
||||
*
|
||||
* @param parameterName The parameter name to use for the placeholder (optional, defaults to ":path")
|
||||
* @return A `WHERE` clause fragment to implement a JSON path match criterion
|
||||
* @throws DocumentException If called against a SQLite database
|
||||
*/
|
||||
fun jsonPathMatches(parameterName: String = ":path") =
|
||||
when (Configuration.dialect("create JSON path match WHERE clause")) {
|
||||
Dialect.POSTGRESQL -> "jsonb_path_exists(data, $parameterName::jsonpath)"
|
||||
Dialect.SQLITE -> throw DocumentException("JSON path match is not supported")
|
||||
}
|
||||
}
|
@ -8,6 +8,10 @@ import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* Unit tests for the `AutoId` enum
|
||||
*/
|
||||
@DisplayName("AutoId")
|
||||
class AutoIdTest {
|
||||
|
||||
@Test
|
||||
|
@ -5,6 +5,10 @@ import org.junit.jupiter.api.Test
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* Unit tests for the `Comparison` class
|
||||
*/
|
||||
@DisplayName("Comparison")
|
||||
class ComparisonTest {
|
||||
|
||||
@Test
|
||||
|
@ -7,6 +7,10 @@ import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* Unit tests for the `Configuration` object
|
||||
*/
|
||||
@DisplayName("Configuration")
|
||||
class ConfigurationTest {
|
||||
|
||||
@Test
|
||||
|
@ -6,6 +6,10 @@ import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* Unit tests for the `Dialect` enum
|
||||
*/
|
||||
@DisplayName("Dialect")
|
||||
class DialectTest {
|
||||
|
||||
@Test
|
||||
|
@ -4,6 +4,10 @@ import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
/**
|
||||
* Unit tests for the `FieldMatch` enum
|
||||
*/
|
||||
@DisplayName("FieldMatch")
|
||||
class FieldMatchTest {
|
||||
|
||||
@Test
|
||||
|
@ -8,6 +8,10 @@ import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotSame
|
||||
import kotlin.test.assertNull
|
||||
|
||||
/**
|
||||
* Unit tests for the `Field` class
|
||||
*/
|
||||
@DisplayName("Field")
|
||||
class FieldTest {
|
||||
|
||||
/**
|
||||
|
@ -4,6 +4,10 @@ import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
/**
|
||||
* Unit tests for the `Op` enum
|
||||
*/
|
||||
@DisplayName("Op")
|
||||
class OpTest {
|
||||
|
||||
@Test
|
||||
|
@ -4,6 +4,10 @@ import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
/**
|
||||
* Unit tests for the `ParameterName` class
|
||||
*/
|
||||
@DisplayName("ParameterName")
|
||||
class ParameterNameTest {
|
||||
|
||||
@Test
|
||||
|
@ -6,6 +6,10 @@ import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
/**
|
||||
* Unit tests for the `Parameter` class
|
||||
*/
|
||||
@DisplayName("Parameter")
|
||||
class ParameterTest {
|
||||
|
||||
@Test
|
||||
|
@ -6,6 +6,10 @@ import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotSame
|
||||
import kotlin.test.assertSame
|
||||
|
||||
/**
|
||||
* Unit tests for the `Parameters` object
|
||||
*/
|
||||
@DisplayName("Parameters")
|
||||
class ParametersTest {
|
||||
|
||||
@Test
|
||||
|
@ -1,685 +0,0 @@
|
||||
package solutions.bitbadger.documents
|
||||
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class QueryTest {
|
||||
|
||||
/** Test table name */
|
||||
private val tbl = "test_table"
|
||||
|
||||
/** Dummy connection string for PostgreSQL */
|
||||
private val pg = ":postgresql:"
|
||||
|
||||
/** Dummy connection string for SQLite */
|
||||
private val lite = ":sqlite:"
|
||||
|
||||
/**
|
||||
* Clear the connection string (resets Dialect)
|
||||
*/
|
||||
@AfterEach
|
||||
fun cleanUp() {
|
||||
Configuration.connectionString = null
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("statementWhere generates correctly")
|
||||
fun statementWhere() =
|
||||
assertEquals("x WHERE y", Query.statementWhere("x", "y"), "Statements not combined correctly")
|
||||
|
||||
// ~~~ Where ~~~
|
||||
|
||||
@Test
|
||||
@DisplayName("Where.byFields is blank when given no fields")
|
||||
fun whereByFieldsBlankIfEmpty() =
|
||||
assertEquals("", Query.Where.byFields(listOf()))
|
||||
|
||||
@Test
|
||||
@DisplayName("Where.byFields generates one numeric field (PostgreSQL)")
|
||||
fun whereByFieldsOneFieldPostgres() {
|
||||
Configuration.connectionString = pg
|
||||
assertEquals("(data->>'it')::numeric = :that", Query.Where.byFields(listOf(Field.equal("it", 9, ":that"))))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Where.byFields generates one alphanumeric field (PostgreSQL)")
|
||||
fun whereByFieldsOneAlphaFieldPostgres() {
|
||||
Configuration.connectionString = pg
|
||||
assertEquals("data->>'it' = :that", Query.Where.byFields(listOf(Field.equal("it", "", ":that"))))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Where.byFields generates one field (SQLite)")
|
||||
fun whereByFieldsOneFieldSQLite() {
|
||||
Configuration.connectionString = lite
|
||||
assertEquals("data->>'it' = :that", Query.Where.byFields(listOf(Field.equal("it", "", ":that"))))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Where.byFields generates multiple fields w/ default match (PostgreSQL)")
|
||||
fun whereByFieldsMultipleDefaultPostgres() {
|
||||
Configuration.connectionString = pg
|
||||
assertEquals(
|
||||
"data->>'1' = :one AND (data->>'2')::numeric = :two AND data->>'3' = :three",
|
||||
Query.Where.byFields(
|
||||
listOf(Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three"))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Where.byFields generates multiple fields w/ default match (SQLite)")
|
||||
fun whereByFieldsMultipleDefaultSQLite() {
|
||||
Configuration.connectionString = lite
|
||||
assertEquals(
|
||||
"data->>'1' = :one AND data->>'2' = :two AND data->>'3' = :three",
|
||||
Query.Where.byFields(
|
||||
listOf(Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three"))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Where.byFields generates multiple fields w/ ANY match (PostgreSQL)")
|
||||
fun whereByFieldsMultipleAnyPostgres() {
|
||||
Configuration.connectionString = pg
|
||||
assertEquals(
|
||||
"data->>'1' = :one OR (data->>'2')::numeric = :two OR data->>'3' = :three",
|
||||
Query.Where.byFields(
|
||||
listOf(Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three")),
|
||||
FieldMatch.ANY
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Where.byFields generates multiple fields w/ ANY match (SQLite)")
|
||||
fun whereByFieldsMultipleAnySQLite() {
|
||||
Configuration.connectionString = lite
|
||||
assertEquals(
|
||||
"data->>'1' = :one OR data->>'2' = :two OR data->>'3' = :three",
|
||||
Query.Where.byFields(
|
||||
listOf(Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three")),
|
||||
FieldMatch.ANY
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Where.byId generates defaults for alphanumeric key (PostgreSQL)")
|
||||
fun whereByIdDefaultAlphaPostgres() {
|
||||
Configuration.connectionString = pg
|
||||
assertEquals("data->>'id' = :id", Query.Where.byId(docId = ""))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Where.byId generates defaults for numeric key (PostgreSQL)")
|
||||
fun whereByIdDefaultNumericPostgres() {
|
||||
Configuration.connectionString = pg
|
||||
assertEquals("(data->>'id')::numeric = :id", Query.Where.byId(docId = 5))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Where.byId generates defaults (SQLite)")
|
||||
fun whereByIdDefaultSQLite() {
|
||||
Configuration.connectionString = lite
|
||||
assertEquals("data->>'id' = :id", Query.Where.byId(docId = ""))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Where.byId generates named ID (PostgreSQL)")
|
||||
fun whereByIdDefaultNamedPostgres() {
|
||||
Configuration.connectionString = pg
|
||||
assertEquals("data->>'id' = :key", Query.Where.byId<String>(":key"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Where.byId generates named ID (SQLite)")
|
||||
fun whereByIdDefaultNamedSQLite() {
|
||||
Configuration.connectionString = lite
|
||||
assertEquals("data->>'id' = :key", Query.Where.byId<String>(":key"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Where.jsonContains generates defaults (PostgreSQL)")
|
||||
fun whereJsonContainsDefaultPostgres() {
|
||||
Configuration.connectionString = pg
|
||||
assertEquals("data @> :criteria", Query.Where.jsonContains())
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Where.jsonContains generates named parameter (PostgreSQL)")
|
||||
fun whereJsonContainsNamedPostgres() {
|
||||
Configuration.connectionString = pg
|
||||
assertEquals("data @> :it", Query.Where.jsonContains(":it"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Where.jsonContains fails (SQLite)")
|
||||
fun whereJsonContainsFailsSQLite() {
|
||||
Configuration.connectionString = lite
|
||||
assertThrows<DocumentException> { Query.Where.jsonContains() }
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Where.jsonPathMatch generates defaults (PostgreSQL)")
|
||||
fun whereJsonPathMatchDefaultPostgres() {
|
||||
Configuration.connectionString = pg
|
||||
assertEquals("jsonb_path_exists(data, :path::jsonpath)", Query.Where.jsonPathMatches())
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Where.jsonPathMatch generates named parameter (PostgreSQL)")
|
||||
fun whereJsonPathMatchNamedPostgres() {
|
||||
Configuration.connectionString = pg
|
||||
assertEquals("jsonb_path_exists(data, :jp::jsonpath)", Query.Where.jsonPathMatches(":jp"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Where.jsonPathMatch fails (SQLite)")
|
||||
fun whereJsonPathFailsSQLite() {
|
||||
Configuration.connectionString = lite
|
||||
assertThrows<DocumentException> { Query.Where.jsonPathMatches() }
|
||||
}
|
||||
|
||||
// ~~~ root functions ~~~
|
||||
|
||||
@Test
|
||||
@DisplayName("byId generates a numeric ID query (PostgreSQL)")
|
||||
fun byIdNumericPostgres() {
|
||||
Configuration.connectionString = pg
|
||||
assertEquals("test WHERE (data->>'id')::numeric = :id", Query.byId("test", 9))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byId generates an alphanumeric ID query (PostgreSQL)")
|
||||
fun byIdAlphaPostgres() {
|
||||
Configuration.connectionString = pg
|
||||
assertEquals("unit WHERE data->>'id' = :id", Query.byId("unit", "18"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byId generates ID query (SQLite)")
|
||||
fun byIdSQLite() {
|
||||
Configuration.connectionString = lite
|
||||
assertEquals("yo WHERE data->>'id' = :id", Query.byId("yo", 27))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byFields generates default field query (PostgreSQL)")
|
||||
fun byFieldsMultipleDefaultPostgres() {
|
||||
Configuration.connectionString = pg
|
||||
assertEquals(
|
||||
"this WHERE data->>'a' = :the_a AND (data->>'b')::numeric = :b_value",
|
||||
Query.byFields("this", listOf(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")))
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byFields generates default field query (SQLite)")
|
||||
fun byFieldsMultipleDefaultSQLite() {
|
||||
Configuration.connectionString = lite
|
||||
assertEquals(
|
||||
"this WHERE data->>'a' = :the_a AND data->>'b' = :b_value",
|
||||
Query.byFields("this", listOf(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")))
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byFields generates ANY field query (PostgreSQL)")
|
||||
fun byFieldsMultipleAnyPostgres() {
|
||||
Configuration.connectionString = pg
|
||||
assertEquals(
|
||||
"that WHERE data->>'a' = :the_a OR (data->>'b')::numeric = :b_value",
|
||||
Query.byFields(
|
||||
"that", listOf(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")),
|
||||
FieldMatch.ANY
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byFields generates ANY field query (SQLite)")
|
||||
fun byFieldsMultipleAnySQLite() {
|
||||
Configuration.connectionString = lite
|
||||
assertEquals(
|
||||
"that WHERE data->>'a' = :the_a OR data->>'b' = :b_value",
|
||||
Query.byFields(
|
||||
"that", listOf(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")),
|
||||
FieldMatch.ANY
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// ~~~ Definition ~~~
|
||||
|
||||
@Test
|
||||
@DisplayName("Definition.ensureTableFor generates correctly")
|
||||
fun ensureTableFor() =
|
||||
assertEquals(
|
||||
"CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)",
|
||||
Query.Definition.ensureTableFor("my.table", "JSONB"), "CREATE TABLE statement not constructed correctly"
|
||||
)
|
||||
|
||||
@Test
|
||||
@DisplayName("Definition.ensureTable generates correctly (PostgreSQL)")
|
||||
fun ensureTablePostgres() {
|
||||
Configuration.connectionString = pg
|
||||
assertEquals("CREATE TABLE IF NOT EXISTS $tbl (data JSONB NOT NULL)", Query.Definition.ensureTable(tbl))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Definition.ensureTable generates correctly (SQLite)")
|
||||
fun ensureTableSQLite() {
|
||||
Configuration.connectionString = lite
|
||||
assertEquals("CREATE TABLE IF NOT EXISTS $tbl (data TEXT NOT NULL)", Query.Definition.ensureTable(tbl))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Definition.ensureTable fails when no dialect is set")
|
||||
fun ensureTableFailsUnknown() {
|
||||
assertThrows<DocumentException> { Query.Definition.ensureTable(tbl) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Definition.ensureKey generates correctly with schema")
|
||||
fun ensureKeyWithSchema() =
|
||||
assertEquals(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'id'))",
|
||||
Query.Definition.ensureKey("test.table", Dialect.POSTGRESQL),
|
||||
"CREATE INDEX for key statement with schema not constructed correctly"
|
||||
)
|
||||
|
||||
@Test
|
||||
@DisplayName("Definition.ensureKey generates correctly without schema")
|
||||
fun ensureKeyWithoutSchema() =
|
||||
assertEquals(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS idx_${tbl}_key ON $tbl ((data->>'id'))",
|
||||
Query.Definition.ensureKey(tbl, Dialect.SQLITE),
|
||||
"CREATE INDEX for key statement without schema not constructed correctly"
|
||||
)
|
||||
|
||||
@Test
|
||||
@DisplayName("Definition.ensureIndexOn generates multiple fields and directions")
|
||||
fun ensureIndexOnMultipleFields() =
|
||||
assertEquals(
|
||||
"CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table ((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)",
|
||||
Query.Definition.ensureIndexOn(
|
||||
"test.table", "gibberish", listOf("taco", "guac DESC", "salsa ASC"),
|
||||
Dialect.POSTGRESQL
|
||||
),
|
||||
"CREATE INDEX for multiple field statement not constructed correctly"
|
||||
)
|
||||
|
||||
@Test
|
||||
@DisplayName("Definition.ensureIndexOn generates nested PostgreSQL field")
|
||||
fun ensureIndexOnNestedPostgres() =
|
||||
assertEquals(
|
||||
"CREATE INDEX IF NOT EXISTS idx_${tbl}_nest ON $tbl ((data#>>'{a,b,c}'))",
|
||||
Query.Definition.ensureIndexOn(tbl, "nest", listOf("a.b.c"), Dialect.POSTGRESQL),
|
||||
"CREATE INDEX for nested PostgreSQL field incorrect"
|
||||
)
|
||||
|
||||
@Test
|
||||
@DisplayName("Definition.ensureIndexOn generates nested SQLite field")
|
||||
fun ensureIndexOnNestedSQLite() =
|
||||
assertEquals(
|
||||
"CREATE INDEX IF NOT EXISTS idx_${tbl}_nest ON $tbl ((data->'a'->'b'->>'c'))",
|
||||
Query.Definition.ensureIndexOn(tbl, "nest", listOf("a.b.c"), Dialect.SQLITE),
|
||||
"CREATE INDEX for nested SQLite field incorrect"
|
||||
)
|
||||
|
||||
// ~~~ root functions ~~~
|
||||
|
||||
@Test
|
||||
@DisplayName("insert generates no auto ID (PostgreSQL)")
|
||||
fun insertNoAutoPostgres() {
|
||||
Configuration.connectionString = pg
|
||||
assertEquals("INSERT INTO $tbl VALUES (:data)", Query.insert(tbl))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("insert generates no auto ID (SQLite)")
|
||||
fun insertNoAutoSQLite() {
|
||||
Configuration.connectionString = lite
|
||||
assertEquals("INSERT INTO $tbl VALUES (:data)", Query.insert(tbl))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("insert generates auto number (PostgreSQL)")
|
||||
fun insertAutoNumberPostgres() {
|
||||
Configuration.connectionString = pg
|
||||
assertEquals(
|
||||
"INSERT INTO $tbl VALUES (:data::jsonb || ('{\"id\":' " +
|
||||
"|| (SELECT COALESCE(MAX((data->>'id')::numeric), 0) + 1 FROM $tbl) || '}')::jsonb)",
|
||||
Query.insert(tbl, AutoId.NUMBER)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("insert generates auto number (SQLite)")
|
||||
fun insertAutoNumberSQLite() {
|
||||
Configuration.connectionString = lite
|
||||
assertEquals(
|
||||
"INSERT INTO $tbl VALUES (json_set(:data, '$.id', " +
|
||||
"(SELECT coalesce(max(data->>'id'), 0) + 1 FROM $tbl)))",
|
||||
Query.insert(tbl, AutoId.NUMBER)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("insert generates auto UUID (PostgreSQL)")
|
||||
fun insertAutoUUIDPostgres() {
|
||||
Configuration.connectionString = pg
|
||||
val query = Query.insert(tbl, AutoId.UUID)
|
||||
assertTrue(
|
||||
query.startsWith("INSERT INTO $tbl VALUES (:data::jsonb || '{\"id\":\""),
|
||||
"Query start not correct (actual: $query)"
|
||||
)
|
||||
assertTrue(query.endsWith("\"}')"), "Query end not correct")
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("insert generates auto UUID (SQLite)")
|
||||
fun insertAutoUUIDSQLite() {
|
||||
Configuration.connectionString = lite
|
||||
val query = Query.insert(tbl, AutoId.UUID)
|
||||
assertTrue(
|
||||
query.startsWith("INSERT INTO $tbl VALUES (json_set(:data, '$.id', '"),
|
||||
"Query start not correct (actual: $query)"
|
||||
)
|
||||
assertTrue(query.endsWith("'))"), "Query end not correct")
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("insert generates auto random string (PostgreSQL)")
|
||||
fun insertAutoRandomPostgres() {
|
||||
try {
|
||||
Configuration.connectionString = pg
|
||||
Configuration.idStringLength = 8
|
||||
val query = Query.insert(tbl, AutoId.RANDOM_STRING)
|
||||
assertTrue(
|
||||
query.startsWith("INSERT INTO $tbl VALUES (:data::jsonb || '{\"id\":\""),
|
||||
"Query start not correct (actual: $query)"
|
||||
)
|
||||
assertTrue(query.endsWith("\"}')"), "Query end not correct")
|
||||
assertEquals(
|
||||
8,
|
||||
query.replace("INSERT INTO $tbl VALUES (:data::jsonb || '{\"id\":\"", "").replace("\"}')", "").length,
|
||||
"Random string length incorrect"
|
||||
)
|
||||
} finally {
|
||||
Configuration.idStringLength = 16
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("insert generates auto random string (SQLite)")
|
||||
fun insertAutoRandomSQLite() {
|
||||
Configuration.connectionString = lite
|
||||
val query = Query.insert(tbl, AutoId.RANDOM_STRING)
|
||||
assertTrue(
|
||||
query.startsWith("INSERT INTO $tbl VALUES (json_set(:data, '$.id', '"),
|
||||
"Query start not correct (actual: $query)"
|
||||
)
|
||||
assertTrue(query.endsWith("'))"), "Query end not correct")
|
||||
assertEquals(
|
||||
Configuration.idStringLength,
|
||||
query.replace("INSERT INTO $tbl VALUES (json_set(:data, '$.id', '", "").replace("'))", "").length,
|
||||
"Random string length incorrect"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("insert fails when no dialect is set")
|
||||
fun insertFailsUnknown() {
|
||||
assertThrows<DocumentException> { Query.insert(tbl) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("save generates correctly")
|
||||
fun save() {
|
||||
Configuration.connectionString = pg
|
||||
assertEquals(
|
||||
"INSERT INTO $tbl VALUES (:data) ON CONFLICT ((data->>'id')) DO UPDATE SET data = EXCLUDED.data",
|
||||
Query.save(tbl), "INSERT ON CONFLICT UPDATE statement not constructed correctly"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Count.all generates correctly")
|
||||
fun countAll() =
|
||||
assertEquals("SELECT COUNT(*) AS it FROM $tbl", Query.Count.all(tbl), "Count query not constructed correctly")
|
||||
|
||||
@Test
|
||||
@DisplayName("Count.byFields generates correctly (PostgreSQL)")
|
||||
fun countByFieldsPostgres() {
|
||||
Configuration.connectionString = pg
|
||||
assertEquals(
|
||||
"SELECT COUNT(*) AS it FROM $tbl WHERE data->>'test' = :field0",
|
||||
Query.Count.byFields(tbl, listOf(Field.equal("test", "", ":field0"))),
|
||||
"Count query not constructed correctly"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Count.byFields generates correctly (PostgreSQL)")
|
||||
fun countByFieldsSQLite() {
|
||||
Configuration.connectionString = lite
|
||||
assertEquals(
|
||||
"SELECT COUNT(*) AS it FROM $tbl WHERE data->>'test' = :field0",
|
||||
Query.Count.byFields(tbl, listOf(Field.equal("test", "", ":field0"))),
|
||||
"Count query not constructed correctly"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Count.byContains generates correctly (PostgreSQL)")
|
||||
fun countByContainsPostgres() {
|
||||
Configuration.connectionString = pg
|
||||
assertEquals(
|
||||
"SELECT COUNT(*) AS it FROM $tbl WHERE data @> :criteria", Query.Count.byContains(tbl),
|
||||
"Count query not constructed correctly"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Count.byContains fails (SQLite)")
|
||||
fun countByContainsSQLite() {
|
||||
Configuration.connectionString = lite
|
||||
assertThrows<DocumentException> { Query.Count.byContains(tbl) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Count.byJsonPath generates correctly (PostgreSQL)")
|
||||
fun countByJsonPathPostgres() {
|
||||
Configuration.connectionString = pg
|
||||
assertEquals(
|
||||
"SELECT COUNT(*) AS it FROM $tbl WHERE jsonb_path_exists(data, :path::jsonpath)",
|
||||
Query.Count.byJsonPath(tbl), "Count query not constructed correctly"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Count.byJsonPath fails (SQLite)")
|
||||
fun countByJsonPathSQLite() {
|
||||
Configuration.connectionString = lite
|
||||
assertThrows<DocumentException> { Query.Count.byJsonPath(tbl) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Exists.byId generates correctly (PostgreSQL)")
|
||||
fun existsByIdPostgres() {
|
||||
Configuration.connectionString = pg
|
||||
assertEquals(
|
||||
"SELECT EXISTS (SELECT 1 FROM $tbl WHERE data->>'id' = :id) AS it",
|
||||
Query.Exists.byId<String>(tbl), "Exists query not constructed correctly"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Exists.byId generates correctly (SQLite)")
|
||||
fun existsByIdSQLite() {
|
||||
Configuration.connectionString = lite
|
||||
assertEquals(
|
||||
"SELECT EXISTS (SELECT 1 FROM $tbl WHERE data->>'id' = :id) AS it",
|
||||
Query.Exists.byId<String>(tbl), "Exists query not constructed correctly"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Exists.byFields generates correctly (PostgreSQL)")
|
||||
fun existsByFieldsPostgres() {
|
||||
Configuration.connectionString = pg
|
||||
assertEquals(
|
||||
"SELECT EXISTS (SELECT 1 FROM $tbl WHERE (data->>'it')::numeric = :test) AS it",
|
||||
Query.Exists.byFields(tbl, listOf(Field.equal("it", 7, ":test"))),
|
||||
"Exists query not constructed correctly"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Exists.byFields generates correctly (SQLite)")
|
||||
fun existsByFieldsSQLite() {
|
||||
Configuration.connectionString = lite
|
||||
assertEquals(
|
||||
"SELECT EXISTS (SELECT 1 FROM $tbl WHERE data->>'it' = :test) AS it",
|
||||
Query.Exists.byFields(tbl, listOf(Field.equal("it", 7, ":test"))),
|
||||
"Exists query not constructed correctly"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Exists.byContains generates correctly (PostgreSQL)")
|
||||
fun existsByContainsPostgres() {
|
||||
Configuration.connectionString = pg
|
||||
assertEquals(
|
||||
"SELECT EXISTS (SELECT 1 FROM $tbl WHERE data @> :criteria) AS it", Query.Exists.byContains(tbl),
|
||||
"Exists query not constructed correctly"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Exists.byContains fails (SQLite)")
|
||||
fun existsByContainsSQLite() {
|
||||
Configuration.connectionString = lite
|
||||
assertThrows<DocumentException> { Query.Exists.byContains(tbl) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Exists.byJsonPath generates correctly (PostgreSQL)")
|
||||
fun existsByJsonPathPostgres() {
|
||||
Configuration.connectionString = pg
|
||||
assertEquals(
|
||||
"SELECT EXISTS (SELECT 1 FROM $tbl WHERE jsonb_path_exists(data, :path::jsonpath)) AS it",
|
||||
Query.Exists.byJsonPath(tbl), "Exists query not constructed correctly"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Exists.byJsonPath fails (SQLite)")
|
||||
fun existsByJsonPathSQLite() {
|
||||
Configuration.connectionString = lite
|
||||
assertThrows<DocumentException> { Query.Exists.byJsonPath(tbl) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Find.all generates correctly")
|
||||
fun findAll() =
|
||||
assertEquals("SELECT data FROM $tbl", Query.Find.all(tbl), "Find query not constructed correctly")
|
||||
|
||||
@Test
|
||||
@DisplayName("update generates successfully")
|
||||
fun update() =
|
||||
assertEquals("UPDATE $tbl SET data = :data", Query.update(tbl), "Update query not constructed correctly")
|
||||
|
||||
// @Test
|
||||
// @DisplayName("delete generates successfully")
|
||||
// fun delete() =
|
||||
// assertEquals("DELETE FROM $tbl", Query.delete(tbl), "Delete query not constructed correctly")
|
||||
|
||||
@Test
|
||||
@DisplayName("orderBy generates for no fields")
|
||||
fun orderByNone() {
|
||||
assertEquals("", Query.orderBy(listOf(), Dialect.POSTGRESQL), "ORDER BY should have been blank (PostgreSQL)")
|
||||
assertEquals("", Query.orderBy(listOf(), Dialect.SQLITE), "ORDER BY should have been blank (SQLite)")
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("orderBy generates single, no direction for PostgreSQL")
|
||||
fun orderBySinglePostgres() =
|
||||
assertEquals(
|
||||
" ORDER BY data->>'TestField'",
|
||||
Query.orderBy(listOf(Field.named("TestField")), Dialect.POSTGRESQL), "ORDER BY not constructed correctly"
|
||||
)
|
||||
|
||||
@Test
|
||||
@DisplayName("orderBy generates single, no direction for SQLite")
|
||||
fun orderBySingleSQLite() =
|
||||
assertEquals(
|
||||
" ORDER BY data->>'TestField'", Query.orderBy(listOf(Field.named("TestField")), Dialect.SQLITE),
|
||||
"ORDER BY not constructed correctly"
|
||||
)
|
||||
|
||||
@Test
|
||||
@DisplayName("orderBy generates multiple with direction for PostgreSQL")
|
||||
fun orderByMultiplePostgres() =
|
||||
assertEquals(
|
||||
" ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC",
|
||||
Query.orderBy(
|
||||
listOf(Field.named("Nested.Test.Field DESC"), Field.named("AnotherField"), Field.named("It DESC")),
|
||||
Dialect.POSTGRESQL
|
||||
),
|
||||
"ORDER BY not constructed correctly"
|
||||
)
|
||||
|
||||
@Test
|
||||
@DisplayName("orderBy generates multiple with direction for SQLite")
|
||||
fun orderByMultipleSQLite() =
|
||||
assertEquals(
|
||||
" ORDER BY data->'Nested'->'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC",
|
||||
Query.orderBy(
|
||||
listOf(Field.named("Nested.Test.Field DESC"), Field.named("AnotherField"), Field.named("It DESC")),
|
||||
Dialect.SQLITE
|
||||
),
|
||||
"ORDER BY not constructed correctly"
|
||||
)
|
||||
|
||||
@Test
|
||||
@DisplayName("orderBy generates numeric ordering PostgreSQL")
|
||||
fun orderByNumericPostgres() =
|
||||
assertEquals(
|
||||
" ORDER BY (data->>'Test')::numeric",
|
||||
Query.orderBy(listOf(Field.named("n:Test")), Dialect.POSTGRESQL), "ORDER BY not constructed correctly"
|
||||
)
|
||||
|
||||
@Test
|
||||
@DisplayName("orderBy generates numeric ordering for SQLite")
|
||||
fun orderByNumericSQLite() =
|
||||
assertEquals(
|
||||
" ORDER BY data->>'Test'", Query.orderBy(listOf(Field.named("n:Test")), Dialect.SQLITE),
|
||||
"ORDER BY not constructed correctly"
|
||||
)
|
||||
|
||||
@Test
|
||||
@DisplayName("orderBy generates case-insensitive ordering for PostgreSQL")
|
||||
fun orderByCIPostgres() =
|
||||
assertEquals(
|
||||
" ORDER BY LOWER(data#>>'{Test,Field}') DESC NULLS FIRST",
|
||||
Query.orderBy(listOf(Field.named("i:Test.Field DESC NULLS FIRST")), Dialect.POSTGRESQL),
|
||||
"ORDER BY not constructed correctly"
|
||||
)
|
||||
|
||||
@Test
|
||||
@DisplayName("orderBy generates case-insensitive ordering for SQLite")
|
||||
fun orderByCISQLite() =
|
||||
assertEquals(
|
||||
" ORDER BY data->'Test'->>'Field' COLLATE NOCASE ASC NULLS LAST",
|
||||
Query.orderBy(listOf(Field.named("i:Test.Field ASC NULLS LAST")), Dialect.SQLITE),
|
||||
"ORDER BY not constructed correctly"
|
||||
)
|
||||
}
|
87
src/test/kotlin/query/CountTest.kt
Normal file
87
src/test/kotlin/query/CountTest.kt
Normal file
@ -0,0 +1,87 @@
|
||||
package solutions.bitbadger.documents.query
|
||||
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import solutions.bitbadger.documents.*
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
/**
|
||||
* Unit tests for the `Count` object
|
||||
*/
|
||||
@DisplayName("Count (Query)")
|
||||
class CountTest {
|
||||
|
||||
/** Test table name */
|
||||
private val tbl = "test_table"
|
||||
|
||||
/**
|
||||
* Clear the connection string (resets Dialect)
|
||||
*/
|
||||
@AfterEach
|
||||
fun cleanUp() {
|
||||
Configuration.dialectValue = null
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("all generates correctly")
|
||||
fun all() =
|
||||
assertEquals("SELECT COUNT(*) AS it FROM $tbl", Count.all(tbl), "Count query not constructed correctly")
|
||||
|
||||
@Test
|
||||
@DisplayName("byFields generates correctly (PostgreSQL)")
|
||||
fun byFieldsPostgres() {
|
||||
Configuration.dialectValue = Dialect.POSTGRESQL
|
||||
assertEquals(
|
||||
"SELECT COUNT(*) AS it FROM $tbl WHERE data->>'test' = :field0",
|
||||
Count.byFields(tbl, listOf(Field.equal("test", "", ":field0"))),
|
||||
"Count query not constructed correctly"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byFields generates correctly (PostgreSQL)")
|
||||
fun byFieldsSQLite() {
|
||||
Configuration.dialectValue = Dialect.SQLITE
|
||||
assertEquals(
|
||||
"SELECT COUNT(*) AS it FROM $tbl WHERE data->>'test' = :field0",
|
||||
Count.byFields(tbl, listOf(Field.equal("test", "", ":field0"))),
|
||||
"Count query not constructed correctly"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byContains generates correctly (PostgreSQL)")
|
||||
fun byContainsPostgres() {
|
||||
Configuration.dialectValue = Dialect.POSTGRESQL
|
||||
assertEquals(
|
||||
"SELECT COUNT(*) AS it FROM $tbl WHERE data @> :criteria", Count.byContains(tbl),
|
||||
"Count query not constructed correctly"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byContains fails (SQLite)")
|
||||
fun byContainsSQLite() {
|
||||
Configuration.dialectValue = Dialect.SQLITE
|
||||
assertThrows<DocumentException> { Count.byContains(tbl) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byJsonPath generates correctly (PostgreSQL)")
|
||||
fun byJsonPathPostgres() {
|
||||
Configuration.dialectValue = Dialect.POSTGRESQL
|
||||
assertEquals(
|
||||
"SELECT COUNT(*) AS it FROM $tbl WHERE jsonb_path_exists(data, :path::jsonpath)",
|
||||
Count.byJsonPath(tbl), "Count query not constructed correctly"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byJsonPath fails (SQLite)")
|
||||
fun byJsonPathSQLite() {
|
||||
Configuration.dialectValue = Dialect.SQLITE
|
||||
assertThrows<DocumentException> { Count.byJsonPath(tbl) }
|
||||
}
|
||||
}
|
104
src/test/kotlin/query/DefinitionTest.kt
Normal file
104
src/test/kotlin/query/DefinitionTest.kt
Normal file
@ -0,0 +1,104 @@
|
||||
package solutions.bitbadger.documents.query
|
||||
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import solutions.bitbadger.documents.Configuration
|
||||
import solutions.bitbadger.documents.Dialect
|
||||
import solutions.bitbadger.documents.DocumentException
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
/**
|
||||
* Unit tests for the `Definition` object
|
||||
*/
|
||||
@DisplayName("Definition (Query)")
|
||||
class DefinitionTest {
|
||||
|
||||
/** Test table name */
|
||||
private val tbl = "test_table"
|
||||
|
||||
/**
|
||||
* Clear the connection string (resets Dialect)
|
||||
*/
|
||||
@AfterEach
|
||||
fun cleanUp() {
|
||||
Configuration.dialectValue = null
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ensureTableFor generates correctly")
|
||||
fun ensureTableFor() =
|
||||
assertEquals(
|
||||
"CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)",
|
||||
Definition.ensureTableFor("my.table", "JSONB"), "CREATE TABLE statement not constructed correctly"
|
||||
)
|
||||
|
||||
@Test
|
||||
@DisplayName("ensureTable generates correctly (PostgreSQL)")
|
||||
fun ensureTablePostgres() {
|
||||
Configuration.dialectValue = Dialect.POSTGRESQL
|
||||
assertEquals("CREATE TABLE IF NOT EXISTS $tbl (data JSONB NOT NULL)", Definition.ensureTable(tbl))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ensureTable generates correctly (SQLite)")
|
||||
fun ensureTableSQLite() {
|
||||
Configuration.dialectValue = Dialect.SQLITE
|
||||
assertEquals("CREATE TABLE IF NOT EXISTS $tbl (data TEXT NOT NULL)", Definition.ensureTable(tbl))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ensureTable fails when no dialect is set")
|
||||
fun ensureTableFailsUnknown() {
|
||||
assertThrows<DocumentException> { Definition.ensureTable(tbl) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ensureKey generates correctly with schema")
|
||||
fun ensureKeyWithSchema() =
|
||||
assertEquals(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'id'))",
|
||||
Definition.ensureKey("test.table", Dialect.POSTGRESQL),
|
||||
"CREATE INDEX for key statement with schema not constructed correctly"
|
||||
)
|
||||
|
||||
@Test
|
||||
@DisplayName("ensureKey generates correctly without schema")
|
||||
fun ensureKeyWithoutSchema() =
|
||||
assertEquals(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS idx_${tbl}_key ON $tbl ((data->>'id'))",
|
||||
Definition.ensureKey(tbl, Dialect.SQLITE),
|
||||
"CREATE INDEX for key statement without schema not constructed correctly"
|
||||
)
|
||||
|
||||
@Test
|
||||
@DisplayName("ensureIndexOn generates multiple fields and directions")
|
||||
fun ensureIndexOnMultipleFields() =
|
||||
assertEquals(
|
||||
"CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table ((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)",
|
||||
Definition.ensureIndexOn(
|
||||
"test.table", "gibberish", listOf("taco", "guac DESC", "salsa ASC"),
|
||||
Dialect.POSTGRESQL
|
||||
),
|
||||
"CREATE INDEX for multiple field statement not constructed correctly"
|
||||
)
|
||||
|
||||
@Test
|
||||
@DisplayName("ensureIndexOn generates nested PostgreSQL field")
|
||||
fun ensureIndexOnNestedPostgres() =
|
||||
assertEquals(
|
||||
"CREATE INDEX IF NOT EXISTS idx_${tbl}_nest ON $tbl ((data#>>'{a,b,c}'))",
|
||||
Definition.ensureIndexOn(tbl, "nest", listOf("a.b.c"), Dialect.POSTGRESQL),
|
||||
"CREATE INDEX for nested PostgreSQL field incorrect"
|
||||
)
|
||||
|
||||
@Test
|
||||
@DisplayName("ensureIndexOn generates nested SQLite field")
|
||||
fun ensureIndexOnNestedSQLite() =
|
||||
assertEquals(
|
||||
"CREATE INDEX IF NOT EXISTS idx_${tbl}_nest ON $tbl ((data->'a'->'b'->>'c'))",
|
||||
Definition.ensureIndexOn(tbl, "nest", listOf("a.b.c"), Dialect.SQLITE),
|
||||
"CREATE INDEX for nested SQLite field incorrect"
|
||||
)
|
||||
}
|
147
src/test/kotlin/query/DocumentTest.kt
Normal file
147
src/test/kotlin/query/DocumentTest.kt
Normal file
@ -0,0 +1,147 @@
|
||||
package solutions.bitbadger.documents.query
|
||||
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import solutions.bitbadger.documents.*
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* Unit tests for the `Document` object
|
||||
*/
|
||||
@DisplayName("Document (Query)")
|
||||
class DocumentTest {
|
||||
|
||||
/** Test table name */
|
||||
private val tbl = "test_table"
|
||||
|
||||
/**
|
||||
* Clear the connection string (resets Dialect)
|
||||
*/
|
||||
@AfterEach
|
||||
fun cleanUp() {
|
||||
Configuration.dialectValue = null
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("insert generates no auto ID (PostgreSQL)")
|
||||
fun insertNoAutoPostgres() {
|
||||
Configuration.dialectValue = Dialect.POSTGRESQL
|
||||
assertEquals("INSERT INTO $tbl VALUES (:data)", Document.insert(tbl))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("insert generates no auto ID (SQLite)")
|
||||
fun insertNoAutoSQLite() {
|
||||
Configuration.dialectValue = Dialect.SQLITE
|
||||
assertEquals("INSERT INTO $tbl VALUES (:data)", Document.insert(tbl))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("insert generates auto number (PostgreSQL)")
|
||||
fun insertAutoNumberPostgres() {
|
||||
Configuration.dialectValue = Dialect.POSTGRESQL
|
||||
assertEquals(
|
||||
"INSERT INTO $tbl VALUES (:data::jsonb || ('{\"id\":' " +
|
||||
"|| (SELECT COALESCE(MAX((data->>'id')::numeric), 0) + 1 FROM $tbl) || '}')::jsonb)",
|
||||
Document.insert(tbl, AutoId.NUMBER)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("insert generates auto number (SQLite)")
|
||||
fun insertAutoNumberSQLite() {
|
||||
Configuration.dialectValue = Dialect.SQLITE
|
||||
assertEquals(
|
||||
"INSERT INTO $tbl VALUES (json_set(:data, '$.id', " +
|
||||
"(SELECT coalesce(max(data->>'id'), 0) + 1 FROM $tbl)))",
|
||||
Document.insert(tbl, AutoId.NUMBER)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("insert generates auto UUID (PostgreSQL)")
|
||||
fun insertAutoUUIDPostgres() {
|
||||
Configuration.dialectValue = Dialect.POSTGRESQL
|
||||
val query = Document.insert(tbl, AutoId.UUID)
|
||||
assertTrue(
|
||||
query.startsWith("INSERT INTO $tbl VALUES (:data::jsonb || '{\"id\":\""),
|
||||
"Query start not correct (actual: $query)"
|
||||
)
|
||||
assertTrue(query.endsWith("\"}')"), "Query end not correct")
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("insert generates auto UUID (SQLite)")
|
||||
fun insertAutoUUIDSQLite() {
|
||||
Configuration.dialectValue = Dialect.SQLITE
|
||||
val query = Document.insert(tbl, AutoId.UUID)
|
||||
assertTrue(
|
||||
query.startsWith("INSERT INTO $tbl VALUES (json_set(:data, '$.id', '"),
|
||||
"Query start not correct (actual: $query)"
|
||||
)
|
||||
assertTrue(query.endsWith("'))"), "Query end not correct")
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("insert generates auto random string (PostgreSQL)")
|
||||
fun insertAutoRandomPostgres() {
|
||||
try {
|
||||
Configuration.dialectValue = Dialect.POSTGRESQL
|
||||
Configuration.idStringLength = 8
|
||||
val query = Document.insert(tbl, AutoId.RANDOM_STRING)
|
||||
assertTrue(
|
||||
query.startsWith("INSERT INTO $tbl VALUES (:data::jsonb || '{\"id\":\""),
|
||||
"Query start not correct (actual: $query)"
|
||||
)
|
||||
assertTrue(query.endsWith("\"}')"), "Query end not correct")
|
||||
assertEquals(
|
||||
8,
|
||||
query.replace("INSERT INTO $tbl VALUES (:data::jsonb || '{\"id\":\"", "").replace("\"}')", "").length,
|
||||
"Random string length incorrect"
|
||||
)
|
||||
} finally {
|
||||
Configuration.idStringLength = 16
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("insert generates auto random string (SQLite)")
|
||||
fun insertAutoRandomSQLite() {
|
||||
Configuration.dialectValue = Dialect.SQLITE
|
||||
val query = Document.insert(tbl, AutoId.RANDOM_STRING)
|
||||
assertTrue(
|
||||
query.startsWith("INSERT INTO $tbl VALUES (json_set(:data, '$.id', '"),
|
||||
"Query start not correct (actual: $query)"
|
||||
)
|
||||
assertTrue(query.endsWith("'))"), "Query end not correct")
|
||||
assertEquals(
|
||||
Configuration.idStringLength,
|
||||
query.replace("INSERT INTO $tbl VALUES (json_set(:data, '$.id', '", "").replace("'))", "").length,
|
||||
"Random string length incorrect"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("insert fails when no dialect is set")
|
||||
fun insertFailsUnknown() {
|
||||
assertThrows<DocumentException> { Document.insert(tbl) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("save generates correctly")
|
||||
fun save() {
|
||||
Configuration.dialectValue = Dialect.POSTGRESQL
|
||||
assertEquals(
|
||||
"INSERT INTO $tbl VALUES (:data) ON CONFLICT ((data->>'id')) DO UPDATE SET data = EXCLUDED.data",
|
||||
Document.save(tbl), "INSERT ON CONFLICT UPDATE statement not constructed correctly"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("update generates successfully")
|
||||
fun update() =
|
||||
assertEquals("UPDATE $tbl SET data = :data", Document.update(tbl), "Update query not constructed correctly")
|
||||
}
|
102
src/test/kotlin/query/ExistsTest.kt
Normal file
102
src/test/kotlin/query/ExistsTest.kt
Normal file
@ -0,0 +1,102 @@
|
||||
package solutions.bitbadger.documents.query
|
||||
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import solutions.bitbadger.documents.*
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
/**
|
||||
* Unit tests for the `Exists` object
|
||||
*/
|
||||
@DisplayName("Exists (Query)")
|
||||
class ExistsTest {
|
||||
|
||||
/** Test table name */
|
||||
private val tbl = "test_table"
|
||||
|
||||
/**
|
||||
* Clear the connection string (resets Dialect)
|
||||
*/
|
||||
@AfterEach
|
||||
fun cleanUp() {
|
||||
Configuration.dialectValue = null
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byId generates correctly (PostgreSQL)")
|
||||
fun byIdPostgres() {
|
||||
Configuration.dialectValue = Dialect.POSTGRESQL
|
||||
assertEquals(
|
||||
"SELECT EXISTS (SELECT 1 FROM $tbl WHERE data->>'id' = :id) AS it",
|
||||
Exists.byId<String>(tbl), "Exists query not constructed correctly"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byId generates correctly (SQLite)")
|
||||
fun byIdSQLite() {
|
||||
Configuration.dialectValue = Dialect.SQLITE
|
||||
assertEquals(
|
||||
"SELECT EXISTS (SELECT 1 FROM $tbl WHERE data->>'id' = :id) AS it",
|
||||
Exists.byId<String>(tbl), "Exists query not constructed correctly"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byFields generates correctly (PostgreSQL)")
|
||||
fun byFieldsPostgres() {
|
||||
Configuration.dialectValue = Dialect.POSTGRESQL
|
||||
assertEquals(
|
||||
"SELECT EXISTS (SELECT 1 FROM $tbl WHERE (data->>'it')::numeric = :test) AS it",
|
||||
Exists.byFields(tbl, listOf(Field.equal("it", 7, ":test"))),
|
||||
"Exists query not constructed correctly"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byFields generates correctly (SQLite)")
|
||||
fun byFieldsSQLite() {
|
||||
Configuration.dialectValue = Dialect.SQLITE
|
||||
assertEquals(
|
||||
"SELECT EXISTS (SELECT 1 FROM $tbl WHERE data->>'it' = :test) AS it",
|
||||
Exists.byFields(tbl, listOf(Field.equal("it", 7, ":test"))),
|
||||
"Exists query not constructed correctly"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byContains generates correctly (PostgreSQL)")
|
||||
fun byContainsPostgres() {
|
||||
Configuration.dialectValue = Dialect.POSTGRESQL
|
||||
assertEquals(
|
||||
"SELECT EXISTS (SELECT 1 FROM $tbl WHERE data @> :criteria) AS it", Exists.byContains(tbl),
|
||||
"Exists query not constructed correctly"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byContains fails (SQLite)")
|
||||
fun byContainsSQLite() {
|
||||
Configuration.dialectValue = Dialect.SQLITE
|
||||
assertThrows<DocumentException> { Exists.byContains(tbl) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byJsonPath generates correctly (PostgreSQL)")
|
||||
fun byJsonPathPostgres() {
|
||||
Configuration.dialectValue = Dialect.POSTGRESQL
|
||||
assertEquals(
|
||||
"SELECT EXISTS (SELECT 1 FROM $tbl WHERE jsonb_path_exists(data, :path::jsonpath)) AS it",
|
||||
Exists.byJsonPath(tbl), "Exists query not constructed correctly"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byJsonPath fails (SQLite)")
|
||||
fun byJsonPathSQLite() {
|
||||
Configuration.dialectValue = Dialect.SQLITE
|
||||
assertThrows<DocumentException> { Exists.byJsonPath(tbl) }
|
||||
}
|
||||
}
|
110
src/test/kotlin/query/FindTest.kt
Normal file
110
src/test/kotlin/query/FindTest.kt
Normal file
@ -0,0 +1,110 @@
|
||||
package solutions.bitbadger.documents.query
|
||||
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import solutions.bitbadger.documents.Configuration
|
||||
import solutions.bitbadger.documents.Dialect
|
||||
import solutions.bitbadger.documents.DocumentException
|
||||
import solutions.bitbadger.documents.Field
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
/**
|
||||
* Unit tests for the `Exists` object
|
||||
*/
|
||||
@DisplayName("Find (Query)")
|
||||
class FindTest {
|
||||
|
||||
/** Test table name */
|
||||
private val tbl = "test_table"
|
||||
|
||||
/**
|
||||
* Clear the connection string (resets Dialect)
|
||||
*/
|
||||
@AfterEach
|
||||
fun cleanUp() {
|
||||
Configuration.dialectValue = null
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("all generates correctly")
|
||||
fun all() =
|
||||
assertEquals("SELECT data FROM $tbl", Find.all(tbl), "Find query not constructed correctly")
|
||||
|
||||
@Test
|
||||
@DisplayName("byId generates correctly (PostgreSQL)")
|
||||
fun byIdPostgres() {
|
||||
Configuration.dialectValue = Dialect.POSTGRESQL
|
||||
assertEquals(
|
||||
"SELECT data FROM $tbl WHERE data->>'id' = :id",
|
||||
Find.byId<String>(tbl), "Find query not constructed correctly"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byId generates correctly (SQLite)")
|
||||
fun byIdSQLite() {
|
||||
Configuration.dialectValue = Dialect.SQLITE
|
||||
assertEquals(
|
||||
"SELECT data FROM $tbl WHERE data->>'id' = :id",
|
||||
Find.byId<String>(tbl), "Find query not constructed correctly"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byFields generates correctly (PostgreSQL)")
|
||||
fun byFieldsPostgres() {
|
||||
Configuration.dialectValue = Dialect.POSTGRESQL
|
||||
assertEquals(
|
||||
"SELECT data FROM $tbl WHERE data->>'a' = :b AND (data->>'c')::numeric < :d",
|
||||
Find.byFields(tbl, listOf(Field.equal("a", "", ":b"), Field.less("c", 14, ":d"))),
|
||||
"Find query not constructed correctly"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byFields generates correctly (SQLite)")
|
||||
fun byFieldsSQLite() {
|
||||
Configuration.dialectValue = Dialect.SQLITE
|
||||
assertEquals(
|
||||
"SELECT data FROM $tbl WHERE data->>'a' = :b AND data->>'c' < :d",
|
||||
Find.byFields(tbl, listOf(Field.equal("a", "", ":b"), Field.less("c", 14, ":d"))),
|
||||
"Find query not constructed correctly"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byContains generates correctly (PostgreSQL)")
|
||||
fun byContainsPostgres() {
|
||||
Configuration.dialectValue = Dialect.POSTGRESQL
|
||||
assertEquals(
|
||||
"SELECT data FROM $tbl WHERE data @> :criteria", Find.byContains(tbl),
|
||||
"Find query not constructed correctly"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byContains fails (SQLite)")
|
||||
fun byContainsSQLite() {
|
||||
Configuration.dialectValue = Dialect.SQLITE
|
||||
assertThrows<DocumentException> { Find.byContains(tbl) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byJsonPath generates correctly (PostgreSQL)")
|
||||
fun byJsonPathPostgres() {
|
||||
Configuration.dialectValue = Dialect.POSTGRESQL
|
||||
assertEquals(
|
||||
"SELECT data FROM $tbl WHERE jsonb_path_exists(data, :path::jsonpath)", Find.byJsonPath(tbl),
|
||||
"Find query not constructed correctly"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byJsonPath fails (SQLite)")
|
||||
fun byJsonPathSQLite() {
|
||||
Configuration.dialectValue = Dialect.SQLITE
|
||||
assertThrows<DocumentException> { Find.byJsonPath(tbl) }
|
||||
}
|
||||
}
|
175
src/test/kotlin/query/QueryTest.kt
Normal file
175
src/test/kotlin/query/QueryTest.kt
Normal file
@ -0,0 +1,175 @@
|
||||
package solutions.bitbadger.documents.query
|
||||
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import solutions.bitbadger.documents.*
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
/**
|
||||
* Unit tests for the top-level query functions
|
||||
*/
|
||||
@DisplayName("Query")
|
||||
class QueryTest {
|
||||
|
||||
/**
|
||||
* Clear the connection string (resets Dialect)
|
||||
*/
|
||||
@AfterEach
|
||||
fun cleanUp() {
|
||||
Configuration.dialectValue = null
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("statementWhere generates correctly")
|
||||
fun statementWhere() =
|
||||
assertEquals("x WHERE y", statementWhere("x", "y"), "Statements not combined correctly")
|
||||
|
||||
@Test
|
||||
@DisplayName("byId generates a numeric ID query (PostgreSQL)")
|
||||
fun byIdNumericPostgres() {
|
||||
Configuration.dialectValue = Dialect.POSTGRESQL
|
||||
assertEquals("test WHERE (data->>'id')::numeric = :id", byId("test", 9))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byId generates an alphanumeric ID query (PostgreSQL)")
|
||||
fun byIdAlphaPostgres() {
|
||||
Configuration.dialectValue = Dialect.POSTGRESQL
|
||||
assertEquals("unit WHERE data->>'id' = :id", byId("unit", "18"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byId generates ID query (SQLite)")
|
||||
fun byIdSQLite() {
|
||||
Configuration.dialectValue = Dialect.SQLITE
|
||||
assertEquals("yo WHERE data->>'id' = :id", byId("yo", 27))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byFields generates default field query (PostgreSQL)")
|
||||
fun byFieldsMultipleDefaultPostgres() {
|
||||
Configuration.dialectValue = Dialect.POSTGRESQL
|
||||
assertEquals(
|
||||
"this WHERE data->>'a' = :the_a AND (data->>'b')::numeric = :b_value",
|
||||
byFields("this", listOf(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")))
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byFields generates default field query (SQLite)")
|
||||
fun byFieldsMultipleDefaultSQLite() {
|
||||
Configuration.dialectValue = Dialect.SQLITE
|
||||
assertEquals(
|
||||
"this WHERE data->>'a' = :the_a AND data->>'b' = :b_value",
|
||||
byFields("this", listOf(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")))
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byFields generates ANY field query (PostgreSQL)")
|
||||
fun byFieldsMultipleAnyPostgres() {
|
||||
Configuration.dialectValue = Dialect.POSTGRESQL
|
||||
assertEquals(
|
||||
"that WHERE data->>'a' = :the_a OR (data->>'b')::numeric = :b_value",
|
||||
byFields(
|
||||
"that", listOf(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")),
|
||||
FieldMatch.ANY
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byFields generates ANY field query (SQLite)")
|
||||
fun byFieldsMultipleAnySQLite() {
|
||||
Configuration.dialectValue = Dialect.SQLITE
|
||||
assertEquals(
|
||||
"that WHERE data->>'a' = :the_a OR data->>'b' = :b_value",
|
||||
byFields(
|
||||
"that", listOf(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")),
|
||||
FieldMatch.ANY
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("orderBy generates for no fields")
|
||||
fun orderByNone() {
|
||||
assertEquals("", orderBy(listOf(), Dialect.POSTGRESQL), "ORDER BY should have been blank (PostgreSQL)")
|
||||
assertEquals("", orderBy(listOf(), Dialect.SQLITE), "ORDER BY should have been blank (SQLite)")
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("orderBy generates single, no direction for PostgreSQL")
|
||||
fun orderBySinglePostgres() =
|
||||
assertEquals(
|
||||
" ORDER BY data->>'TestField'",
|
||||
orderBy(listOf(Field.named("TestField")), Dialect.POSTGRESQL), "ORDER BY not constructed correctly"
|
||||
)
|
||||
|
||||
@Test
|
||||
@DisplayName("orderBy generates single, no direction for SQLite")
|
||||
fun orderBySingleSQLite() =
|
||||
assertEquals(
|
||||
" ORDER BY data->>'TestField'", orderBy(listOf(Field.named("TestField")), Dialect.SQLITE),
|
||||
"ORDER BY not constructed correctly"
|
||||
)
|
||||
|
||||
@Test
|
||||
@DisplayName("orderBy generates multiple with direction for PostgreSQL")
|
||||
fun orderByMultiplePostgres() =
|
||||
assertEquals(
|
||||
" ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC",
|
||||
orderBy(
|
||||
listOf(Field.named("Nested.Test.Field DESC"), Field.named("AnotherField"), Field.named("It DESC")),
|
||||
Dialect.POSTGRESQL
|
||||
),
|
||||
"ORDER BY not constructed correctly"
|
||||
)
|
||||
|
||||
@Test
|
||||
@DisplayName("orderBy generates multiple with direction for SQLite")
|
||||
fun orderByMultipleSQLite() =
|
||||
assertEquals(
|
||||
" ORDER BY data->'Nested'->'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC",
|
||||
orderBy(
|
||||
listOf(Field.named("Nested.Test.Field DESC"), Field.named("AnotherField"), Field.named("It DESC")),
|
||||
Dialect.SQLITE
|
||||
),
|
||||
"ORDER BY not constructed correctly"
|
||||
)
|
||||
|
||||
@Test
|
||||
@DisplayName("orderBy generates numeric ordering PostgreSQL")
|
||||
fun orderByNumericPostgres() =
|
||||
assertEquals(
|
||||
" ORDER BY (data->>'Test')::numeric",
|
||||
orderBy(listOf(Field.named("n:Test")), Dialect.POSTGRESQL), "ORDER BY not constructed correctly"
|
||||
)
|
||||
|
||||
@Test
|
||||
@DisplayName("orderBy generates numeric ordering for SQLite")
|
||||
fun orderByNumericSQLite() =
|
||||
assertEquals(
|
||||
" ORDER BY data->>'Test'", orderBy(listOf(Field.named("n:Test")), Dialect.SQLITE),
|
||||
"ORDER BY not constructed correctly"
|
||||
)
|
||||
|
||||
@Test
|
||||
@DisplayName("orderBy generates case-insensitive ordering for PostgreSQL")
|
||||
fun orderByCIPostgres() =
|
||||
assertEquals(
|
||||
" ORDER BY LOWER(data#>>'{Test,Field}') DESC NULLS FIRST",
|
||||
orderBy(listOf(Field.named("i:Test.Field DESC NULLS FIRST")), Dialect.POSTGRESQL),
|
||||
"ORDER BY not constructed correctly"
|
||||
)
|
||||
|
||||
@Test
|
||||
@DisplayName("orderBy generates case-insensitive ordering for SQLite")
|
||||
fun orderByCISQLite() =
|
||||
assertEquals(
|
||||
" ORDER BY data->'Test'->>'Field' COLLATE NOCASE ASC NULLS LAST",
|
||||
orderBy(listOf(Field.named("i:Test.Field ASC NULLS LAST")), Dialect.SQLITE),
|
||||
"ORDER BY not constructed correctly"
|
||||
)
|
||||
}
|
176
src/test/kotlin/query/WhereTest.kt
Normal file
176
src/test/kotlin/query/WhereTest.kt
Normal file
@ -0,0 +1,176 @@
|
||||
package solutions.bitbadger.documents.query
|
||||
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import solutions.bitbadger.documents.*
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
/**
|
||||
* Unit tests for the `Where` object
|
||||
*/
|
||||
@DisplayName("Where (Query)")
|
||||
class WhereTest {
|
||||
|
||||
/**
|
||||
* Clear the connection string (resets Dialect)
|
||||
*/
|
||||
@AfterEach
|
||||
fun cleanUp() {
|
||||
Configuration.dialectValue = null
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byFields is blank when given no fields")
|
||||
fun byFieldsBlankIfEmpty() =
|
||||
assertEquals("", Where.byFields(listOf()))
|
||||
|
||||
@Test
|
||||
@DisplayName("byFields generates one numeric field (PostgreSQL)")
|
||||
fun byFieldsOneFieldPostgres() {
|
||||
Configuration.dialectValue = Dialect.POSTGRESQL
|
||||
assertEquals("(data->>'it')::numeric = :that", Where.byFields(listOf(Field.equal("it", 9, ":that"))))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byFields generates one alphanumeric field (PostgreSQL)")
|
||||
fun byFieldsOneAlphaFieldPostgres() {
|
||||
Configuration.dialectValue = Dialect.POSTGRESQL
|
||||
assertEquals("data->>'it' = :that", Where.byFields(listOf(Field.equal("it", "", ":that"))))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byFields generates one field (SQLite)")
|
||||
fun byFieldsOneFieldSQLite() {
|
||||
Configuration.dialectValue = Dialect.SQLITE
|
||||
assertEquals("data->>'it' = :that", Where.byFields(listOf(Field.equal("it", "", ":that"))))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byFields generates multiple fields w/ default match (PostgreSQL)")
|
||||
fun byFieldsMultipleDefaultPostgres() {
|
||||
Configuration.dialectValue = Dialect.POSTGRESQL
|
||||
assertEquals(
|
||||
"data->>'1' = :one AND (data->>'2')::numeric = :two AND data->>'3' = :three",
|
||||
Where.byFields(
|
||||
listOf(Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three"))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byFields generates multiple fields w/ default match (SQLite)")
|
||||
fun byFieldsMultipleDefaultSQLite() {
|
||||
Configuration.dialectValue = Dialect.SQLITE
|
||||
assertEquals(
|
||||
"data->>'1' = :one AND data->>'2' = :two AND data->>'3' = :three",
|
||||
Where.byFields(
|
||||
listOf(Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three"))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byFields generates multiple fields w/ ANY match (PostgreSQL)")
|
||||
fun byFieldsMultipleAnyPostgres() {
|
||||
Configuration.dialectValue = Dialect.POSTGRESQL
|
||||
assertEquals(
|
||||
"data->>'1' = :one OR (data->>'2')::numeric = :two OR data->>'3' = :three",
|
||||
Where.byFields(
|
||||
listOf(Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three")),
|
||||
FieldMatch.ANY
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byFields generates multiple fields w/ ANY match (SQLite)")
|
||||
fun byFieldsMultipleAnySQLite() {
|
||||
Configuration.dialectValue = Dialect.SQLITE
|
||||
assertEquals(
|
||||
"data->>'1' = :one OR data->>'2' = :two OR data->>'3' = :three",
|
||||
Where.byFields(
|
||||
listOf(Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three")),
|
||||
FieldMatch.ANY
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byId generates defaults for alphanumeric key (PostgreSQL)")
|
||||
fun byIdDefaultAlphaPostgres() {
|
||||
Configuration.dialectValue = Dialect.POSTGRESQL
|
||||
assertEquals("data->>'id' = :id", Where.byId(docId = ""))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byId generates defaults for numeric key (PostgreSQL)")
|
||||
fun byIdDefaultNumericPostgres() {
|
||||
Configuration.dialectValue = Dialect.POSTGRESQL
|
||||
assertEquals("(data->>'id')::numeric = :id", Where.byId(docId = 5))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byId generates defaults (SQLite)")
|
||||
fun byIdDefaultSQLite() {
|
||||
Configuration.dialectValue = Dialect.SQLITE
|
||||
assertEquals("data->>'id' = :id", Where.byId(docId = ""))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byId generates named ID (PostgreSQL)")
|
||||
fun byIdDefaultNamedPostgres() {
|
||||
Configuration.dialectValue = Dialect.POSTGRESQL
|
||||
assertEquals("data->>'id' = :key", Where.byId<String>(":key"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("byId generates named ID (SQLite)")
|
||||
fun byIdDefaultNamedSQLite() {
|
||||
Configuration.dialectValue = Dialect.SQLITE
|
||||
assertEquals("data->>'id' = :key", Where.byId<String>(":key"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("jsonContains generates defaults (PostgreSQL)")
|
||||
fun jsonContainsDefaultPostgres() {
|
||||
Configuration.dialectValue = Dialect.POSTGRESQL
|
||||
assertEquals("data @> :criteria", Where.jsonContains())
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("jsonContains generates named parameter (PostgreSQL)")
|
||||
fun jsonContainsNamedPostgres() {
|
||||
Configuration.dialectValue = Dialect.POSTGRESQL
|
||||
assertEquals("data @> :it", Where.jsonContains(":it"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("jsonContains fails (SQLite)")
|
||||
fun jsonContainsFailsSQLite() {
|
||||
Configuration.dialectValue = Dialect.SQLITE
|
||||
assertThrows<DocumentException> { Where.jsonContains() }
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("jsonPathMatches generates defaults (PostgreSQL)")
|
||||
fun jsonPathMatchDefaultPostgres() {
|
||||
Configuration.dialectValue = Dialect.POSTGRESQL
|
||||
assertEquals("jsonb_path_exists(data, :path::jsonpath)", Where.jsonPathMatches())
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("jsonPathMatches generates named parameter (PostgreSQL)")
|
||||
fun jsonPathMatchNamedPostgres() {
|
||||
Configuration.dialectValue = Dialect.POSTGRESQL
|
||||
assertEquals("jsonb_path_exists(data, :jp::jsonpath)", Where.jsonPathMatches(":jp"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("jsonPathMatches fails (SQLite)")
|
||||
fun jsonPathFailsSQLite() {
|
||||
Configuration.dialectValue = Dialect.SQLITE
|
||||
assertThrows<DocumentException> { Where.jsonPathMatches() }
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user