WIP on repo reorg

This commit is contained in:
2025-03-11 20:35:57 -04:00
parent b17ef73ff8
commit e0ec37e8c2
94 changed files with 903 additions and 328 deletions

93
src/common/pom.xml Normal file
View File

@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>solutions.bitbadger.documents</groupId>
<artifactId>common</artifactId>
<version>4.0.0-alpha1-SNAPSHOT</version>
<packaging>jar</packaging>
<parent>
<groupId>solutions.bitbadger</groupId>
<artifactId>documents</artifactId>
<version>4.0.0-alpha1-SNAPSHOT</version>
</parent>
<name>${project.groupId}:${project.artifactId}</name>
<description>Expose a document store interface for PostgreSQL and SQLite (Common Library)</description>
<url>https://bitbadger.solutions/open-source/relational-documents/jvm/</url>
<build>
<sourceDirectory>src/main/kotlin</sourceDirectory>
<testSourceDirectory>src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<phase>process-sources</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<configuration>
<compilerPlugins>
<plugin>kotlinx-serialization</plugin>
</compilerPlugins>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-serialization</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.22.2</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.6.0</version>
<configuration>
<mainClass>MainKt</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>9</source>
<target>9</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,85 @@
package solutions.bitbadger.documents.common
import kotlin.jvm.Throws
import kotlin.reflect.full.*
import kotlin.reflect.jvm.isAccessible
/**
* Strategies for automatic document IDs
*/
enum class AutoId {
/** No automatic IDs will be generated */
DISABLED,
/** Generate a `MAX`-plus-1 numeric ID */
NUMBER,
/** Generate a `UUID` string ID */
UUID,
/** Generate a random hex character string ID */
RANDOM_STRING;
companion object {
/**
* Generate a `UUID` string
*
* @return A `UUID` string
*/
@JvmStatic
fun generateUUID(): String =
java.util.UUID.randomUUID().toString().replace("-", "")
/**
* Generate a string of random hex characters
*
* @param length The length of the string (optional; defaults to configured length)
* @return A string of random hex characters of the requested length
*/
@JvmStatic
fun generateRandomString(length: Int? = null): String =
(length ?: Configuration.idStringLength).let { len ->
kotlin.random.Random.nextBytes((len + 2) / 2)
.joinToString("") { String.format("%02x", it) }
.substring(0, len)
}
/**
* Determine if a document needs an automatic ID applied
*
* @param strategy The auto ID strategy for which the document is evaluated
* @param document The document whose need of an automatic ID should be determined
* @param idProp The name of the document property containing the ID
* @return `true` if the document needs an automatic ID, `false` if not
* @throws DocumentException If bad input prevents the determination
*/
@Throws(DocumentException::class)
@JvmStatic
fun <T> needsAutoId(strategy: AutoId, document: T, idProp: String): Boolean {
if (document == null) throw DocumentException("document cannot be null")
if (strategy == DISABLED) return false
val id = document!!::class.memberProperties.find { it.name == idProp }?.apply { isAccessible = true }
if (id == null) throw DocumentException("$idProp not found in document")
if (strategy == NUMBER) {
return when (id.returnType) {
Byte::class.createType() -> id.call(document) == 0.toByte()
Short::class.createType() -> id.call(document) == 0.toShort()
Int::class.createType() -> id.call(document) == 0
Long::class.createType() -> id.call(document) == 0.toLong()
else -> throw DocumentException("$idProp was not a number; cannot auto-generate number ID")
}
}
val typ = id.returnType.toString()
if (typ.endsWith("String") || typ.endsWith("String!")) {
return id.call(document) == ""
}
throw DocumentException("$idProp was not a string ($typ); cannot auto-generate UUID or random string")
}
}
}

View File

@@ -0,0 +1,68 @@
package solutions.bitbadger.documents.common
/**
* Information required to generate a JSON field comparison
*/
interface Comparison<T> {
/** The operation for the field comparison */
val op: Op
/** The value against which the comparison will be made */
val value: T
/** Whether the value should be considered numeric */
val isNumeric: Boolean
}
/**
* Function to determine if a value is numeric
*
* @param it The value in question
* @return True if it is a numeric type, false if not
*/
private fun <T> isNumeric(it: T) =
it is Byte || it is Short || it is Int || it is Long
/**
* A single-value comparison against a field in a JSON document
*/
class ComparisonSingle<T>(override val op: Op, override val value: T) : Comparison<T> {
init {
when (op) {
Op.BETWEEN, Op.IN, Op.IN_ARRAY ->
throw DocumentException("Cannot use single comparison for multiple values")
else -> { }
}
}
override val isNumeric = isNumeric(value)
override fun toString() =
"$op $value"
}
/**
* A range comparison against a field in a JSON document
*/
class ComparisonBetween<T>(override val value: Pair<T, T>) : Comparison<Pair<T, T>> {
override val op = Op.BETWEEN
override val isNumeric = isNumeric(value.first)
}
/**
* A check within a collection of values
*/
class ComparisonIn<T>(override val value: Collection<T>) : Comparison<Collection<T>> {
override val op = Op.IN
override val isNumeric = !value.isEmpty() && isNumeric(value.elementAt(0))
}
/**
* A check within a collection of values against an array in a document
*/
class ComparisonInArray<T>(override val value: Pair<String, Collection<T>>) : Comparison<Pair<String, Collection<T>>> {
override val op = Op.IN_ARRAY
override val isNumeric = false
}

View File

@@ -0,0 +1,63 @@
package solutions.bitbadger.documents.common
import java.sql.Connection
import java.sql.DriverManager
import kotlin.jvm.Throws
/**
* Configuration for the document library
*/
object Configuration {
/** The field in which a document's ID is stored */
@JvmField
var idField = "id"
/** The automatic ID strategy to use */
@JvmField
var autoIdStrategy = AutoId.DISABLED
/** The length of automatic random hex character string */
@JvmField
var idStringLength = 16
/** The derived dialect value from the connection string */
internal var dialectValue: Dialect? = null
/** The connection string for the JDBC connection */
@JvmStatic
var connectionString: String? = null
set(value) {
field = value
dialectValue = if (value.isNullOrBlank()) null else Dialect.deriveFromConnectionString(value)
}
/**
* Retrieve a new connection to the configured database
*
* @return A new connection to the configured database
* @throws IllegalArgumentException If the connection string is not set before calling this
*/
@JvmStatic
fun dbConn(): Connection {
if (connectionString == null) {
throw IllegalArgumentException("Please provide a connection string before attempting data access")
}
return DriverManager.getConnection(connectionString)
}
/**
* The dialect in use
*
* @param process The process being attempted
* @return The dialect for the current connection
* @throws DocumentException If the dialect has not been set
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun dialect(process: String? = null): Dialect =
dialectValue ?: throw DocumentException(
"Database mode not set" + if (process == null) "" else "; cannot $process"
)
}

View File

@@ -0,0 +1,28 @@
package solutions.bitbadger.documents.common
/**
* The SQL dialect to use when building queries
*/
enum class Dialect {
/** PostgreSQL */
POSTGRESQL,
/** SQLite */
SQLITE;
companion object {
/**
* Derive the dialect from the given connection string
*
* @param connectionString The connection string from which the dialect will be derived
* @return The dialect for the connection string
* @throws DocumentException If the dialect cannot be determined
*/
fun deriveFromConnectionString(connectionString: String): Dialect =
when {
connectionString.contains(":sqlite:") -> SQLITE
connectionString.contains(":postgresql:") -> POSTGRESQL
else -> throw DocumentException("Cannot determine dialect from [$connectionString]")
}
}
}

View File

@@ -0,0 +1,9 @@
package solutions.bitbadger.documents.common
/**
* An exception caused by invalid operations in the document library
*
* @param message The message for the exception
* @param cause The underlying exception (optional)
*/
class DocumentException(message: String, cause: Throwable? = null) : Exception(message, cause)

View File

@@ -0,0 +1,13 @@
package solutions.bitbadger.documents.common
/**
* The type of index to generate for the document
*/
enum class DocumentIndex(val sql: String) {
/** A GIN index with standard operations (all operators supported) */
FULL(""),
/** A GIN index with JSONPath operations (optimized for @>, @?, @@ operators) */
OPTIMIZED(" jsonb_path_ops")
}

View File

@@ -0,0 +1,317 @@
package solutions.bitbadger.documents.common
/**
* A field and its comparison
*
* @property name The name of the field in the JSON document
* @property comparison The comparison to apply against the field
* @property parameterName The name of the parameter to use in the query (optional, generated if missing)
* @property qualifier A table qualifier to use to address the `data` field (useful for multi-table queries)
*/
class Field<T> private constructor(
val name: String,
val comparison: Comparison<T>,
val parameterName: String? = null,
val qualifier: String? = null) {
init {
if (parameterName != null && !parameterName.startsWith(':') && !parameterName.startsWith('@'))
throw DocumentException("Parameter Name must start with : or @ ($name)")
}
/**
* Specify the parameter name for the field
*
* @param paramName The parameter name to use for this field
* @return A new `Field` with the parameter name specified
*/
fun withParameterName(paramName: String) =
Field(name, comparison, paramName, qualifier)
/**
* Specify a qualifier (alias) for the document table
*
* @param alias The table alias for this field
* @return A new `Field` with the table qualifier specified
*/
fun withQualifier(alias: String) =
Field(name, comparison, parameterName, alias)
/**
* Get the path for this field
*
* @param dialect The SQL dialect to use for the path to the JSON field
* @param format Whether the value should be retrieved as JSON or SQL (optional, default SQL)
* @return The path for the field
*/
fun path(dialect: Dialect, format: FieldFormat = FieldFormat.SQL): String =
(if (qualifier == null) "" else "${qualifier}.") + nameToPath(name, dialect, format)
/** Parameters to bind each value of `IN` and `IN_ARRAY` operations */
private val inParameterNames: String
get() {
val values = if (comparison.op == Op.IN) {
comparison.value as Collection<*>
} else {
val parts = comparison.value as Pair<*, *>
parts.second as Collection<*>
}
return List(values.size) { idx -> "${parameterName}_$idx" }.joinToString(", ")
}
/**
* Create a `WHERE` clause fragment for this field
*
* @return The `WHERE` clause for this field
* @throws DocumentException If the field has no parameter name or the database dialect has not been set
*/
fun toWhere(): String {
if (parameterName == null && !listOf(Op.EXISTS, Op.NOT_EXISTS).contains(comparison.op))
throw DocumentException("Parameter for $name must be specified")
val dialect = Configuration.dialect("make field WHERE clause")
val fieldName = path(dialect, if (comparison.op == Op.IN_ARRAY) FieldFormat.JSON else FieldFormat.SQL)
val fieldPath = when (dialect) {
Dialect.POSTGRESQL -> if (comparison.isNumeric) "($fieldName)::numeric" else fieldName
Dialect.SQLITE -> fieldName
}
val criteria = when (comparison.op) {
in listOf(Op.EXISTS, Op.NOT_EXISTS) -> ""
Op.BETWEEN -> " ${parameterName}min AND ${parameterName}max"
Op.IN -> " ($inParameterNames)"
Op.IN_ARRAY -> if (dialect == Dialect.POSTGRESQL) " ARRAY[$inParameterNames]" else ""
else -> " $parameterName"
}
@Suppress("UNCHECKED_CAST")
return if (dialect == Dialect.SQLITE && comparison.op == Op.IN_ARRAY) {
val (table, _) = comparison.value as? Pair<String, *> ?: throw DocumentException("InArray field invalid")
"EXISTS (SELECT 1 FROM json_each($table.data, '$.$name') WHERE value IN ($inParameterNames))"
} else {
"$fieldPath ${comparison.op.sql}$criteria"
}
}
/**
* Append the parameters required for this field
*
* @param existing The existing parameters
* @return The collection with the necessary parameters appended
*/
fun appendParameter(existing: MutableCollection<Parameter<*>>): MutableCollection<Parameter<*>> {
val typ = if (comparison.isNumeric) ParameterType.NUMBER else ParameterType.STRING
when (comparison) {
is ComparisonBetween<*> -> {
existing.add(Parameter("${parameterName}min", typ, comparison.value.first))
existing.add(Parameter("${parameterName}max", typ, comparison.value.second))
}
is ComparisonIn<*> -> {
comparison.value.forEachIndexed { index, item ->
existing.add(Parameter("${parameterName}_$index", typ, item))
}
}
is ComparisonInArray<*> -> {
val mkString = Configuration.dialect("append parameters for InArray") == Dialect.POSTGRESQL
// TODO: I think this is actually Pair<String, Collection<*>>
comparison.value.second.forEachIndexed { index, item ->
if (mkString) {
existing.add(Parameter("${parameterName}_$index", ParameterType.STRING, "$item"))
} else {
existing.add(Parameter("${parameterName}_$index", typ, item))
}
}
}
else -> {
if (comparison.op != Op.EXISTS && comparison.op != Op.NOT_EXISTS) {
existing.add(Parameter(parameterName!!, typ, comparison.value))
}
}
}
return existing
}
override fun toString() =
"Field ${parameterName ?: "<unnamed>"} $comparison${qualifier?.let { " (qualifier $it)" } ?: ""}"
companion object {
/**
* Create a field equality comparison
*
* @param name The name of the field to be compared
* @param value The value for the comparison
* @param paramName The parameter name for the field (optional, defaults to auto-generated)
* @return A `Field` with the given comparison
*/
@JvmStatic
@JvmOverloads
fun <T> equal(name: String, value: T, paramName: String? = null) =
Field(name, ComparisonSingle(Op.EQUAL, value), paramName)
/**
* Create a field greater-than comparison
*
* @param name The name of the field to be compared
* @param value The value for the comparison
* @param paramName The parameter name for the field (optional, defaults to auto-generated)
* @return A `Field` with the given comparison
*/
@JvmStatic
@JvmOverloads
fun <T> greater(name: String, value: T, paramName: String? = null) =
Field(name, ComparisonSingle(Op.GREATER, value), paramName)
/**
* Create a field greater-than-or-equal-to comparison
*
* @param name The name of the field to be compared
* @param value The value for the comparison
* @param paramName The parameter name for the field (optional, defaults to auto-generated)
* @return A `Field` with the given comparison
*/
@JvmStatic
@JvmOverloads
fun <T> greaterOrEqual(name: String, value: T, paramName: String? = null) =
Field(name, ComparisonSingle(Op.GREATER_OR_EQUAL, value), paramName)
/**
* Create a field less-than comparison
*
* @param name The name of the field to be compared
* @param value The value for the comparison
* @param paramName The parameter name for the field (optional, defaults to auto-generated)
* @return A `Field` with the given comparison
*/
@JvmStatic
@JvmOverloads
fun <T> less(name: String, value: T, paramName: String? = null) =
Field(name, ComparisonSingle(Op.LESS, value), paramName)
/**
* Create a field less-than-or-equal-to comparison
*
* @param name The name of the field to be compared
* @param value The value for the comparison
* @param paramName The parameter name for the field (optional, defaults to auto-generated)
* @return A `Field` with the given comparison
*/
@JvmStatic
@JvmOverloads
fun <T> lessOrEqual(name: String, value: T, paramName: String? = null) =
Field(name, ComparisonSingle(Op.LESS_OR_EQUAL, value), paramName)
/**
* Create a field inequality comparison
*
* @param name The name of the field to be compared
* @param value The value for the comparison
* @param paramName The parameter name for the field (optional, defaults to auto-generated)
* @return A `Field` with the given comparison
*/
@JvmStatic
@JvmOverloads
fun <T> notEqual(name: String, value: T, paramName: String? = null) =
Field(name, ComparisonSingle(Op.NOT_EQUAL, value), paramName)
/**
* Create a field range comparison
*
* @param name The name of the field to be compared
* @param minValue The lower value for the comparison
* @param maxValue The upper value for the comparison
* @param paramName The parameter name for the field (optional, defaults to auto-generated)
* @return A `Field` with the given comparison
*/
@JvmStatic
@JvmOverloads
fun <T> between(name: String, minValue: T, maxValue: T, paramName: String? = null) =
Field(name, ComparisonBetween(Pair(minValue, maxValue)), paramName)
/**
* Create a field where any values match (SQL `IN`)
*
* @param name The name of the field to be compared
* @param values The values for the comparison
* @param paramName The parameter name for the field (optional, defaults to auto-generated)
* @return A `Field` with the given comparison
*/
@JvmStatic
@JvmOverloads
fun <T> any(name: String, values: Collection<T>, paramName: String? = null) =
Field(name, ComparisonIn(values), paramName)
/**
* Create a field where values should exist in a document's array
*
* @param name The name of the field to be compared
* @param tableName The name of the document table
* @param values The values for the comparison
* @param paramName The parameter name for the field (optional, defaults to auto-generated)
* @return A `Field` with the given comparison
*/
@JvmStatic
@JvmOverloads
fun <T> inArray(name: String, tableName: String, values: Collection<T>, paramName: String? = null) =
Field(name, ComparisonInArray(Pair(tableName, values)), paramName)
/**
* Create a field where a document field should exist
*
* @param name The name of the field whose existence should be checked
* @return A `Field` with the given comparison
*/
@JvmStatic
fun exists(name: String) =
Field(name, ComparisonSingle(Op.EXISTS, ""))
/**
* Create a field where a document field should not exist
*
* @param name The name of the field whose existence should be checked
* @return A `Field` with the given comparison
*/
@JvmStatic
fun notExists(name: String) =
Field(name, ComparisonSingle(Op.NOT_EXISTS, ""))
/**
* Create a field with a given named comparison (useful for ordering fields)
*
* @param name The name of the field
* @return A `Field` with the given name (comparison equal to an empty string)
*/
@JvmStatic
fun named(name: String) =
Field(name, ComparisonSingle(Op.EQUAL, ""))
/**
* Convert a name to the SQL path for the given dialect
*
* @param name The field name to be translated
* @param dialect The database for which the path should be created
* @param format Whether the field should be retrieved as a JSON value or a SQL value
* @return The path to the JSON field
*/
@JvmStatic
fun nameToPath(name: String, dialect: Dialect, format: FieldFormat): String {
val path = StringBuilder("data")
val extra = if (format == FieldFormat.SQL) ">" else ""
if (name.indexOf('.') > -1) {
if (dialect == Dialect.POSTGRESQL) {
path.append("#>", extra, "'{", name.replace('.', ','), "}'")
} else {
val names = name.split('.').toMutableList()
val last = names.removeLast()
names.forEach { path.append("->'", it, "'") }
path.append("->", extra, "'", last, "'")
}
} else {
path.append("->", extra, "'", name, "'")
}
return path.toString()
}
}
}

View File

@@ -0,0 +1,11 @@
package solutions.bitbadger.documents.common
/**
* The data format for a document field retrieval
*/
enum class FieldFormat {
/** Retrieve the field as a SQL value (string in PostgreSQL, best guess in SQLite */
SQL,
/** Retrieve the field as a JSON value */
JSON
}

View File

@@ -0,0 +1,11 @@
package solutions.bitbadger.documents.common
/**
* How fields should be matched in by-field queries
*/
enum class FieldMatch(val sql: String) {
/** Match any of the field criteria (`OR`) */
ANY("OR"),
/** Match all the field criteria (`AND`) */
ALL("AND"),
}

View File

@@ -0,0 +1,29 @@
package solutions.bitbadger.documents.common
/**
* A comparison operator used for fields
*/
enum class Op(val sql: String) {
/** Compare using equality */
EQUAL("="),
/** Compare using greater-than */
GREATER(">"),
/** Compare using greater-than-or-equal-to */
GREATER_OR_EQUAL(">="),
/** Compare using less-than */
LESS("<"),
/** Compare using less-than-or-equal-to */
LESS_OR_EQUAL("<="),
/** Compare using inequality */
NOT_EQUAL("<>"),
/** Compare between two values */
BETWEEN("BETWEEN"),
/** Compare existence in a list of values */
IN("IN"),
/** Compare overlap between an array and a list of values */
IN_ARRAY("??|"),
/** Compare existence */
EXISTS("IS NOT NULL"),
/** Compare nonexistence */
NOT_EXISTS("IS NULL")
}

View File

@@ -0,0 +1,55 @@
package solutions.bitbadger.documents.common
import java.sql.PreparedStatement
import java.sql.Types
/**
* A parameter to use for a query
*
* @property name The name of the parameter (prefixed with a colon)
* @property type The type of this parameter
* @property value The value of the parameter
*/
class Parameter<T>(val name: String, val type: ParameterType, val value: T) {
init {
if (!name.startsWith(':') && !name.startsWith('@'))
throw DocumentException("Name must start with : or @ ($name)")
}
/**
* Bind this parameter to a prepared statement at the given index
*
* @param stmt The prepared statement to which this parameter should be bound
* @param index The index (1-based) to which the parameter should be bound
*/
fun bind(stmt: PreparedStatement, index: Int) {
when (type) {
ParameterType.NUMBER -> {
when (value) {
null -> stmt.setNull(index, Types.NULL)
is Byte -> stmt.setByte(index, value)
is Short -> stmt.setShort(index, value)
is Int -> stmt.setInt(index, value)
is Long -> stmt.setLong(index, value)
else -> throw DocumentException(
"Number parameter must be Byte, Short, Int, or Long (${value!!::class.simpleName})"
)
}
}
ParameterType.STRING -> {
when (value) {
null -> stmt.setNull(index, Types.NULL)
is String -> stmt.setString(index, value)
else -> stmt.setString(index, value.toString())
}
}
ParameterType.JSON -> stmt.setObject(index, value as String, Types.OTHER)
}
}
override fun toString() =
"$type[$name] = $value"
}

View File

@@ -0,0 +1,18 @@
package solutions.bitbadger.documents.common
/**
* Derive parameter names; each instance wraps a counter to provide names for anonymous fields
*/
class ParameterName {
private var currentIdx = 0
/**
* Derive the parameter name from the current possibly-null string
*
* @param paramName The name of the parameter as specified by the field
* @return The name from the field, if present, or a derived name if missing
*/
fun derive(paramName: String?): String =
paramName ?: ":field${currentIdx++}"
}

View File

@@ -0,0 +1,15 @@
package solutions.bitbadger.documents.common
/**
* The types of parameters supported by the document library
*/
enum class ParameterType {
/** The parameter value is some sort of number (`Byte`, `Short`, `Int`, or `Long`) */
NUMBER,
/** The parameter value is a string */
STRING,
/** The parameter should be JSON-encoded */
JSON,
}

View File

@@ -0,0 +1,49 @@
package solutions.bitbadger.documents.common.query
import solutions.bitbadger.documents.common.Field
import solutions.bitbadger.documents.common.FieldMatch
import solutions.bitbadger.documents.common.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())
}

View File

@@ -0,0 +1,92 @@
package solutions.bitbadger.documents.common.query
import solutions.bitbadger.documents.common.*
/**
* 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)
* @param dialect The dialect to generate (optional, used in place of current)
* @return A query to create a document table
*/
fun ensureTable(tableName: String, dialect: Dialect? = null) =
when (dialect ?: 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 (optional, used in place of current)
* @return A query to create the field index
*/
fun ensureIndexOn(
tableName: String,
indexName: String,
fields: Collection<String>,
dialect: Dialect? = null
): String {
val (_, tbl) = splitSchemaAndTable(tableName)
val mode = dialect ?: Configuration.dialect("create index $tbl.$indexName")
val jsonFields = fields.joinToString(", ") {
val parts = it.split(' ')
val direction = if (parts.size > 1) " ${parts[1]}" else ""
"(" + Field.nameToPath(parts[0], mode, 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 (optional, used in place of current)
* @return A query to create the key index
*/
fun ensureKey(tableName: String, dialect: Dialect? = null) =
ensureIndexOn(tableName, "key", listOf(Configuration.idField), dialect).replace("INDEX", "UNIQUE INDEX")
/**
* Create a document-wide index on a table (PostgreSQL only)
*
* @param tableName The name of the table on which the document index should be created
* @param indexType The type of index to be created
* @return The SQL statement to create an index on JSON documents in the specified table
* @throws DocumentException If the database mode is not PostgreSQL
*/
fun ensureDocumentIndexOn(tableName: String, indexType: DocumentIndex): String {
if (Configuration.dialect("create document index query") != Dialect.POSTGRESQL) {
throw DocumentException("'Document indexes are only supported on PostgreSQL")
}
val (_, tbl) = splitSchemaAndTable(tableName)
return "CREATE INDEX IF NOT EXISTS idx_${tbl}_document ON $tableName USING GIN (data${indexType.sql})"
}
}

View File

@@ -0,0 +1,60 @@
package solutions.bitbadger.documents.common.query
import solutions.bitbadger.documents.common.Field
import solutions.bitbadger.documents.common.FieldMatch
import solutions.bitbadger.documents.common.query.byFields as byFieldsBase
import solutions.bitbadger.documents.common.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)
* @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())
}

View File

@@ -0,0 +1,58 @@
package solutions.bitbadger.documents.common.query
import solutions.bitbadger.documents.common.AutoId
import solutions.bitbadger.documents.common.Configuration
import solutions.bitbadger.documents.common.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"
}

View File

@@ -0,0 +1,59 @@
package solutions.bitbadger.documents.common.query
import solutions.bitbadger.documents.common.Field
import solutions.bitbadger.documents.common.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())
}

View File

@@ -0,0 +1,60 @@
package solutions.bitbadger.documents.common.query
import solutions.bitbadger.documents.common.Field
import solutions.bitbadger.documents.common.FieldMatch
import solutions.bitbadger.documents.common.query.byId as byIdBase
import solutions.bitbadger.documents.common.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())
}

View File

@@ -0,0 +1,65 @@
package solutions.bitbadger.documents.common.query
import solutions.bitbadger.documents.common.Configuration
import solutions.bitbadger.documents.common.Dialect
import solutions.bitbadger.documents.common.Field
import solutions.bitbadger.documents.common.FieldMatch
import solutions.bitbadger.documents.common.query.byFields as byFieldsBase
import solutions.bitbadger.documents.common.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 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 byJsonPath(tableName: String) =
statementWhere(patch(tableName), Where.jsonPathMatches())
}

View File

@@ -0,0 +1,77 @@
package solutions.bitbadger.documents.common.query
import solutions.bitbadger.documents.common.Configuration
import solutions.bitbadger.documents.common.Dialect
import solutions.bitbadger.documents.common.Field
import solutions.bitbadger.documents.common.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? = null): String {
val mode = dialect ?: Configuration.dialect("generate ORDER BY clause")
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 (mode) {
Dialect.POSTGRESQL -> "(${fld.path(mode)})::numeric"
Dialect.SQLITE -> fld.path(mode)
}
}
field.name.startsWith("i:") -> Field.named(field.name.substring(2)).path(mode).let { p ->
when (mode) {
Dialect.POSTGRESQL -> "LOWER($p)"
Dialect.SQLITE -> "$p COLLATE NOCASE"
}
}
else -> field.path(mode)
}
"$path${direction ?: ""}"
}
return " ORDER BY $orderFields"
}

View File

@@ -0,0 +1,74 @@
package solutions.bitbadger.documents.common.query
import solutions.bitbadger.documents.common.*
import solutions.bitbadger.documents.common.query.byFields as byFieldsBase
import solutions.bitbadger.documents.common.query.byId as byIdBase
/**
* Functions to create queries to remove fields from documents
*/
object RemoveFields {
/**
* Create a query to remove fields based on the given parameters
*
* @param tableName The name of the table in which documents should have fields removed
* @param toRemove The parameters for the fields to be removed
* @return A query to remove fields from documents in the given table
*/
private fun removeFields(tableName: String, toRemove: Collection<Parameter<*>>) =
when (Configuration.dialect("generate field removal query")) {
Dialect.POSTGRESQL -> "UPDATE $tableName SET data = data - ${toRemove.elementAt(0).name}::text[]"
Dialect.SQLITE -> toRemove.joinToString(", ") { it.name }.let {
"UPDATE $tableName SET data = json_remove(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 toRemove The parameters for the fields to be removed
* @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, toRemove: Collection<Parameter<*>>, docId: TKey? = null) =
byIdBase(removeFields(tableName, toRemove), 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 toRemove The parameters for the fields to be removed
* @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,
toRemove: Collection<Parameter<*>>,
fields: Collection<Field<*>>,
howMatched: FieldMatch? = null
) =
byFieldsBase(removeFields(tableName, toRemove), 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
* @param toRemove The parameters for the fields to be removed
* @return A query to patch JSON documents by JSON containment
*/
fun byContains(tableName: String, toRemove: Collection<Parameter<*>>) =
statementWhere(removeFields(tableName, toRemove), 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
* @param toRemove The parameters for the fields to be removed
* @return A query to patch JSON documents by JSON path match
*/
fun byJsonPath(tableName: String, toRemove: Collection<Parameter<*>>) =
statementWhere(removeFields(tableName, toRemove), Where.jsonPathMatches())
}

View File

@@ -0,0 +1,58 @@
package solutions.bitbadger.documents.common.query
import solutions.bitbadger.documents.common.Configuration
import solutions.bitbadger.documents.common.Dialect
import solutions.bitbadger.documents.common.DocumentException
import solutions.bitbadger.documents.common.Field
import solutions.bitbadger.documents.common.FieldMatch
/**
* 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")
}
}

View File

@@ -0,0 +1,217 @@
package solutions.bitbadger.documents.common.java;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import solutions.bitbadger.documents.common.AutoId;
import solutions.bitbadger.documents.common.DocumentException;
import solutions.bitbadger.documents.java.testDocs.*;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for the `AutoId` enum
*/
@DisplayName("Java | Common | AutoId")
final public class AutoIdTest {
@Test
@DisplayName("Generates a UUID string")
public void generateUUID() {
assertEquals(32, AutoId.generateUUID().length(), "The UUID should have been a 32-character string");
}
@Test
@DisplayName("Generates a random hex character string of an even length")
public void generateRandomStringEven() {
final String result = AutoId.generateRandomString(8);
assertEquals(8, result.length(), "There should have been 8 characters in " + result);
}
@Test
@DisplayName("Generates a random hex character string of an odd length")
public void generateRandomStringOdd() {
final String result = AutoId.generateRandomString(11);
assertEquals(11, result.length(), "There should have been 11 characters in " + result);
}
@Test
@DisplayName("Generates different random hex character strings")
public void generateRandomStringIsRandom() {
final String result1 = AutoId.generateRandomString(16);
final String result2 = AutoId.generateRandomString(16);
assertNotEquals(result1, result2, "There should have been 2 different strings generated");
}
@Test
@DisplayName("needsAutoId fails for null document")
public void needsAutoIdFailsForNullDocument() {
assertThrows(DocumentException.class, () -> AutoId.needsAutoId(AutoId.DISABLED, null, "id"));
}
@Test
@DisplayName("needsAutoId fails for missing ID property")
public void needsAutoIdFailsForMissingId() {
assertThrows(DocumentException.class, () -> AutoId.needsAutoId(AutoId.UUID, new IntIdClass(0), "Id"));
}
@Test
@DisplayName("needsAutoId returns false if disabled")
public void needsAutoIdFalseIfDisabled() {
try {
assertFalse(AutoId.needsAutoId(AutoId.DISABLED, "", ""), "Disabled Auto ID should always return false");
} catch (DocumentException ex) {
fail(ex);
}
}
@Test
@DisplayName("needsAutoId returns true for Number strategy and byte ID of 0")
public void needsAutoIdTrueForByteWithZero() {
try {
assertTrue(AutoId.needsAutoId(AutoId.NUMBER, new ByteIdClass((byte) 0), "id"),
"Number Auto ID with 0 should return true");
} catch (DocumentException ex) {
fail(ex);
}
}
@Test
@DisplayName("needsAutoId returns false for Number strategy and byte ID of non-0")
public void needsAutoIdFalseForByteWithNonZero() {
try {
assertFalse(AutoId.needsAutoId(AutoId.NUMBER, new ByteIdClass((byte) 77), "id"),
"Number Auto ID with 77 should return false");
} catch (DocumentException ex) {
fail(ex);
}
}
@Test
@DisplayName("needsAutoId returns true for Number strategy and short ID of 0")
public void needsAutoIdTrueForShortWithZero() {
try {
assertTrue(AutoId.needsAutoId(AutoId.NUMBER, new ShortIdClass((short) 0), "id"),
"Number Auto ID with 0 should return true");
} catch (DocumentException ex) {
fail(ex);
}
}
@Test
@DisplayName("needsAutoId returns false for Number strategy and short ID of non-0")
public void needsAutoIdFalseForShortWithNonZero() {
try {
assertFalse(AutoId.needsAutoId(AutoId.NUMBER, new ShortIdClass((short) 31), "id"),
"Number Auto ID with 31 should return false");
} catch (DocumentException ex) {
fail(ex);
}
}
@Test
@DisplayName("needsAutoId returns true for Number strategy and int ID of 0")
public void needsAutoIdTrueForIntWithZero() {
try {
assertTrue(AutoId.needsAutoId(AutoId.NUMBER, new IntIdClass(0), "id"),
"Number Auto ID with 0 should return true");
} catch (DocumentException ex) {
fail(ex);
}
}
@Test
@DisplayName("needsAutoId returns false for Number strategy and int ID of non-0")
public void needsAutoIdFalseForIntWithNonZero() {
try {
assertFalse(AutoId.needsAutoId(AutoId.NUMBER, new IntIdClass(6), "id"),
"Number Auto ID with 6 should return false");
} catch (DocumentException ex) {
fail(ex);
}
}
@Test
@DisplayName("needsAutoId returns true for Number strategy and long ID of 0")
public void needsAutoIdTrueForLongWithZero() {
try {
assertTrue(AutoId.needsAutoId(AutoId.NUMBER, new LongIdClass(0L), "id"),
"Number Auto ID with 0 should return true");
} catch (DocumentException ex) {
fail(ex);
}
}
@Test
@DisplayName("needsAutoId returns false for Number strategy and long ID of non-0")
public void needsAutoIdFalseForLongWithNonZero() {
try {
assertFalse(AutoId.needsAutoId(AutoId.NUMBER, new LongIdClass(2L), "id"),
"Number Auto ID with 2 should return false");
} catch (DocumentException ex) {
fail(ex);
}
}
@Test
@DisplayName("needsAutoId fails for Number strategy and non-number ID")
public void needsAutoIdFailsForNumberWithStringId() {
assertThrows(DocumentException.class, () -> AutoId.needsAutoId(AutoId.NUMBER, new StringIdClass(""), "id"));
}
@Test
@DisplayName("needsAutoId returns true for UUID strategy and blank ID")
public void needsAutoIdTrueForUUIDWithBlank() {
try {
assertTrue(AutoId.needsAutoId(AutoId.UUID, new StringIdClass(""), "id"),
"UUID Auto ID with blank should return true");
} catch (DocumentException ex) {
fail(ex);
}
}
@Test
@DisplayName("needsAutoId returns false for UUID strategy and non-blank ID")
public void needsAutoIdFalseForUUIDNotBlank() {
try {
assertFalse(AutoId.needsAutoId(AutoId.UUID, new StringIdClass("howdy"), "id"),
"UUID Auto ID with non-blank should return false");
} catch (DocumentException ex) {
fail(ex);
}
}
@Test
@DisplayName("needsAutoId fails for UUID strategy and non-string ID")
public void needsAutoIdFailsForUUIDNonString() {
assertThrows(DocumentException.class, () -> AutoId.needsAutoId(AutoId.UUID, new IntIdClass(5), "id"));
}
@Test
@DisplayName("needsAutoId returns true for Random String strategy and blank ID")
public void needsAutoIdTrueForRandomWithBlank() {
try {
assertTrue(AutoId.needsAutoId(AutoId.RANDOM_STRING, new StringIdClass(""), "id"),
"Random String Auto ID with blank should return true");
} catch (DocumentException ex) {
fail(ex);
}
}
@Test
@DisplayName("needsAutoId returns false for Random String strategy and non-blank ID")
public void needsAutoIdFalseForRandomNotBlank() {
try {
assertFalse(AutoId.needsAutoId(AutoId.RANDOM_STRING, new StringIdClass("full"), "id"),
"Random String Auto ID with non-blank should return false");
} catch (DocumentException ex) {
fail(ex);
}
}
@Test
@DisplayName("needsAutoId fails for Random String strategy and non-string ID")
public void needsAutoIdFailsForRandomNonString() {
assertThrows(DocumentException.class,
() -> AutoId.needsAutoId(AutoId.RANDOM_STRING, new ShortIdClass((short) 55), "id"));
}
}

View File

@@ -0,0 +1,26 @@
package solutions.bitbadger.documents.common.java;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import solutions.bitbadger.documents.common.DocumentIndex;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* Unit tests for the `DocumentIndex` enum
*/
@DisplayName("Java | Common | DocumentIndex")
final public class DocumentIndexTest {
@Test
@DisplayName("FULL uses proper SQL")
public void fullSQL() {
assertEquals("", DocumentIndex.FULL.getSql(), "The SQL for Full is incorrect");
}
@Test
@DisplayName("OPTIMIZED uses proper SQL")
public void optimizedSQL() {
assertEquals(" jsonb_path_ops", DocumentIndex.OPTIMIZED.getSql(), "The SQL for Optimized is incorrect");
}
}

View File

@@ -0,0 +1,26 @@
package solutions.bitbadger.documents.common.java;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import solutions.bitbadger.documents.common.FieldMatch;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* Unit tests for the `FieldMatch` enum
*/
@DisplayName("Java | Common | FieldMatch")
final public class FieldMatchTest {
@Test
@DisplayName("ANY uses proper SQL")
public void any() {
assertEquals("OR", FieldMatch.ANY.getSql(), "ANY should use OR");
}
@Test
@DisplayName("ALL uses proper SQL")
public void all() {
assertEquals("AND", FieldMatch.ALL.getSql(), "ALL should use AND");
}
}

View File

@@ -0,0 +1,629 @@
package solutions.bitbadger.documents.common.java;
import kotlin.Pair;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import solutions.bitbadger.documents.common.*;
import java.util.Collection;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for the `Field` class
*/
@DisplayName("Java | Common | Field")
final public class FieldTest {
/**
* Clear the connection string (resets Dialect)
*/
@AfterEach
public void cleanUp() {
Configuration.setConnectionString(null);
}
// ~~~ INSTANCE METHODS ~~~
@Test
@DisplayName("withParameterName fails for invalid name")
public void withParamNameFails() {
assertThrows(DocumentException.class, () -> Field.equal("it", "").withParameterName("2424"));
}
@Test
@DisplayName("withParameterName works with colon prefix")
public void withParamNameColon() {
Field<String> field = Field.equal("abc", "22").withQualifier("me");
Field<String> withParam = field.withParameterName(":test");
assertNotSame(field, withParam, "A new Field instance should have been created");
assertEquals(field.getName(), withParam.getName(), "Name should have been preserved");
assertEquals(field.getComparison(), withParam.getComparison(), "Comparison should have been preserved");
assertEquals(":test", withParam.getParameterName(), "Parameter name not set correctly");
assertEquals(field.getQualifier(), withParam.getQualifier(), "Qualifier should have been preserved");
}
@Test
@DisplayName("withParameterName works with at-sign prefix")
public void withParamNameAtSign() {
Field<String> field = Field.equal("def", "44");
Field<String> withParam = field.withParameterName("@unit");
assertNotSame(field, withParam, "A new Field instance should have been created");
assertEquals(field.getName(), withParam.getName(), "Name should have been preserved");
assertEquals(field.getComparison(), withParam.getComparison(), "Comparison should have been preserved");
assertEquals("@unit", withParam.getParameterName(), "Parameter name not set correctly");
assertEquals(field.getQualifier(), withParam.getQualifier(), "Qualifier should have been preserved");
}
@Test
@DisplayName("withQualifier sets qualifier correctly")
public void withQualifier() {
Field<String> field = Field.equal("j", "k");
Field<String> withQual = field.withQualifier("test");
assertNotSame(field, withQual, "A new Field instance should have been created");
assertEquals(field.getName(), withQual.getName(), "Name should have been preserved");
assertEquals(field.getComparison(), withQual.getComparison(), "Comparison should have been preserved");
assertEquals(field.getParameterName(), withQual.getParameterName(),
"Parameter Name should have been preserved");
assertEquals("test", withQual.getQualifier(), "Qualifier not set correctly");
}
@Test
@DisplayName("path generates for simple unqualified PostgreSQL field")
public void pathPostgresSimpleUnqualified() {
assertEquals("data->>'SomethingCool'",
Field.greaterOrEqual("SomethingCool", 18).path(Dialect.POSTGRESQL, FieldFormat.SQL),
"Path not correct");
}
@Test
@DisplayName("path generates for simple qualified PostgreSQL field")
public void pathPostgresSimpleQualified() {
assertEquals("this.data->>'SomethingElse'",
Field.less("SomethingElse", 9).withQualifier("this").path(Dialect.POSTGRESQL, FieldFormat.SQL),
"Path not correct");
}
@Test
@DisplayName("path generates for nested unqualified PostgreSQL field")
public void pathPostgresNestedUnqualified() {
assertEquals("data#>>'{My,Nested,Field}'",
Field.equal("My.Nested.Field", "howdy").path(Dialect.POSTGRESQL, FieldFormat.SQL), "Path not correct");
}
@Test
@DisplayName("path generates for nested qualified PostgreSQL field")
public void pathPostgresNestedQualified() {
assertEquals("bird.data#>>'{Nest,Away}'",
Field.equal("Nest.Away", "doc").withQualifier("bird").path(Dialect.POSTGRESQL, FieldFormat.SQL),
"Path not correct");
}
@Test
@DisplayName("path generates for simple unqualified SQLite field")
public void pathSQLiteSimpleUnqualified() {
assertEquals("data->>'SomethingCool'",
Field.greaterOrEqual("SomethingCool", 18).path(Dialect.SQLITE, FieldFormat.SQL), "Path not correct");
}
@Test
@DisplayName("path generates for simple qualified SQLite field")
public void pathSQLiteSimpleQualified() {
assertEquals("this.data->>'SomethingElse'",
Field.less("SomethingElse", 9).withQualifier("this").path(Dialect.SQLITE, FieldFormat.SQL),
"Path not correct");
}
@Test
@DisplayName("path generates for nested unqualified SQLite field")
public void pathSQLiteNestedUnqualified() {
assertEquals("data->'My'->'Nested'->>'Field'",
Field.equal("My.Nested.Field", "howdy").path(Dialect.SQLITE, FieldFormat.SQL), "Path not correct");
}
@Test
@DisplayName("path generates for nested qualified SQLite field")
public void pathSQLiteNestedQualified() {
assertEquals("bird.data->'Nest'->>'Away'",
Field.equal("Nest.Away", "doc").withQualifier("bird").path(Dialect.SQLITE, FieldFormat.SQL),
"Path not correct");
}
@Test
@DisplayName("toWhere generates for exists w/o qualifier (PostgreSQL)")
public void toWhereExistsNoQualPostgres() {
Configuration.setConnectionString(":postgresql:");
assertEquals("data->>'that_field' IS NOT NULL", Field.exists("that_field").toWhere(),
"Field WHERE clause not generated correctly");
}
@Test
@DisplayName("toWhere generates for exists w/o qualifier (SQLite)")
public void toWhereExistsNoQualSQLite() {
Configuration.setConnectionString(":sqlite:");
assertEquals("data->>'that_field' IS NOT NULL", Field.exists("that_field").toWhere(),
"Field WHERE clause not generated correctly");
}
@Test
@DisplayName("toWhere generates for not-exists w/o qualifier (PostgreSQL)")
public void toWhereNotExistsNoQualPostgres() {
Configuration.setConnectionString(":postgresql:");
assertEquals("data->>'a_field' IS NULL", Field.notExists("a_field").toWhere(),
"Field WHERE clause not generated correctly");
}
@Test
@DisplayName("toWhere generates for not-exists w/o qualifier (SQLite)")
public void toWhereNotExistsNoQualSQLite() {
Configuration.setConnectionString(":sqlite:");
assertEquals("data->>'a_field' IS NULL", Field.notExists("a_field").toWhere(),
"Field WHERE clause not generated correctly");
}
@Test
@DisplayName("toWhere generates for BETWEEN w/o qualifier, numeric range (PostgreSQL)")
public void toWhereBetweenNoQualNumericPostgres() {
Configuration.setConnectionString(":postgresql:");
assertEquals("(data->>'age')::numeric BETWEEN @agemin AND @agemax",
Field.between("age", 13, 17, "@age").toWhere(), "Field WHERE clause not generated correctly");
}
@Test
@DisplayName("toWhere generates for BETWEEN w/o qualifier, alphanumeric range (PostgreSQL)")
public void toWhereBetweenNoQualAlphaPostgres() {
Configuration.setConnectionString(":postgresql:");
assertEquals("data->>'city' BETWEEN :citymin AND :citymax",
Field.between("city", "Atlanta", "Chicago", ":city").toWhere(),
"Field WHERE clause not generated correctly");
}
@Test
@DisplayName("toWhere generates for BETWEEN w/o qualifier (SQLite)")
public void toWhereBetweenNoQualSQLite() {
Configuration.setConnectionString(":sqlite:");
assertEquals("data->>'age' BETWEEN @agemin AND @agemax", Field.between("age", 13, 17, "@age").toWhere(),
"Field WHERE clause not generated correctly");
}
@Test
@DisplayName("toWhere generates for BETWEEN w/ qualifier, numeric range (PostgreSQL)")
public void toWhereBetweenQualNumericPostgres() {
Configuration.setConnectionString(":postgresql:");
assertEquals("(test.data->>'age')::numeric BETWEEN @agemin AND @agemax",
Field.between("age", 13, 17, "@age").withQualifier("test").toWhere(),
"Field WHERE clause not generated correctly");
}
@Test
@DisplayName("toWhere generates for BETWEEN w/ qualifier, alphanumeric range (PostgreSQL)")
public void toWhereBetweenQualAlphaPostgres() {
Configuration.setConnectionString(":postgresql:");
assertEquals("unit.data->>'city' BETWEEN :citymin AND :citymax",
Field.between("city", "Atlanta", "Chicago", ":city").withQualifier("unit").toWhere(),
"Field WHERE clause not generated correctly");
}
@Test
@DisplayName("toWhere generates for BETWEEN w/ qualifier (SQLite)")
public void toWhereBetweenQualSQLite() {
Configuration.setConnectionString(":sqlite:");
assertEquals("my.data->>'age' BETWEEN @agemin AND @agemax",
Field.between("age", 13, 17, "@age").withQualifier("my").toWhere(),
"Field WHERE clause not generated correctly");
}
@Test
@DisplayName("toWhere generates for IN/any, numeric values (PostgreSQL)")
public void toWhereAnyNumericPostgres() {
Configuration.setConnectionString(":postgresql:");
assertEquals("(data->>'even')::numeric IN (:nbr_0, :nbr_1, :nbr_2)",
Field.any("even", List.of(2, 4, 6), ":nbr").toWhere(),
"Field WHERE clause not generated correctly");
}
@Test
@DisplayName("toWhere generates for IN/any, alphanumeric values (PostgreSQL)")
public void toWhereAnyAlphaPostgres() {
Configuration.setConnectionString(":postgresql:");
assertEquals("data->>'test' IN (:city_0, :city_1)",
Field.any("test", List.of("Atlanta", "Chicago"), ":city").toWhere(),
"Field WHERE clause not generated correctly");
}
@Test
@DisplayName("toWhere generates for IN/any (SQLite)")
public void toWhereAnySQLite() {
Configuration.setConnectionString(":sqlite:");
assertEquals("data->>'test' IN (:city_0, :city_1)",
Field.any("test", List.of("Atlanta", "Chicago"), ":city").toWhere(),
"Field WHERE clause not generated correctly");
}
@Test
@DisplayName("toWhere generates for inArray (PostgreSQL)")
public void toWhereInArrayPostgres() {
Configuration.setConnectionString(":postgresql:");
assertEquals("data->'even' ??| ARRAY[:it_0, :it_1, :it_2, :it_3]",
Field.inArray("even", "tbl", List.of(2, 4, 6, 8), ":it").toWhere(),
"Field WHERE clause not generated correctly");
}
@Test
@DisplayName("toWhere generates for inArray (SQLite)")
public void toWhereInArraySQLite() {
Configuration.setConnectionString(":sqlite:");
assertEquals("EXISTS (SELECT 1 FROM json_each(tbl.data, '$.test') WHERE value IN (:city_0, :city_1))",
Field.inArray("test", "tbl", List.of("Atlanta", "Chicago"), ":city").toWhere(),
"Field WHERE clause not generated correctly");
}
@Test
@DisplayName("toWhere generates for others w/o qualifier (PostgreSQL)")
public void toWhereOtherNoQualPostgres() {
Configuration.setConnectionString(":postgresql:");
assertEquals("data->>'some_field' = :value", Field.equal("some_field", "", ":value").toWhere(),
"Field WHERE clause not generated correctly");
}
@Test
@DisplayName("toWhere generates for others w/o qualifier (SQLite)")
public void toWhereOtherNoQualSQLite() {
Configuration.setConnectionString(":sqlite:");
assertEquals("data->>'some_field' = :value", Field.equal("some_field", "", ":value").toWhere(),
"Field WHERE clause not generated correctly");
}
@Test
@DisplayName("toWhere generates no-parameter w/ qualifier (PostgreSQL)")
public void toWhereNoParamWithQualPostgres() {
Configuration.setConnectionString(":postgresql:");
assertEquals("test.data->>'no_field' IS NOT NULL", Field.exists("no_field").withQualifier("test").toWhere(),
"Field WHERE clause not generated correctly");
}
@Test
@DisplayName("toWhere generates no-parameter w/ qualifier (SQLite)")
public void toWhereNoParamWithQualSQLite() {
Configuration.setConnectionString(":sqlite:");
assertEquals("test.data->>'no_field' IS NOT NULL", Field.exists("no_field").withQualifier("test").toWhere(),
"Field WHERE clause not generated correctly");
}
@Test
@DisplayName("toWhere generates parameter w/ qualifier (PostgreSQL)")
public void toWhereParamWithQualPostgres() {
Configuration.setConnectionString(":postgresql:");
assertEquals("(q.data->>'le_field')::numeric <= :it",
Field.lessOrEqual("le_field", 18, ":it").withQualifier("q").toWhere(),
"Field WHERE clause not generated correctly");
}
@Test
@DisplayName("toWhere generates parameter w/ qualifier (SQLite)")
public void toWhereParamWithQualSQLite() {
Configuration.setConnectionString(":sqlite:");
assertEquals("q.data->>'le_field' <= :it",
Field.lessOrEqual("le_field", 18, ":it").withQualifier("q").toWhere(),
"Field WHERE clause not generated correctly");
}
// ~~~ STATIC TESTS ~~~
@Test
@DisplayName("equal constructs a field w/o parameter name")
public void equalCtor() {
Field<Integer> field = Field.equal("Test", 14);
assertEquals("Test", field.getName(), "Field name not filled correctly");
assertEquals(Op.EQUAL, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals(14, field.getComparison().getValue(), "Field comparison value not filled correctly");
assertNull(field.getParameterName(), "The parameter name should have been null");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("equal constructs a field w/ parameter name")
public void equalParameterCtor() {
Field<Integer> field = Field.equal("Test", 14, ":w");
assertEquals("Test", field.getName(), "Field name not filled correctly");
assertEquals(Op.EQUAL, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals(14, field.getComparison().getValue(), "Field comparison value not filled correctly");
assertEquals(":w", field.getParameterName(), "Field parameter name not filled correctly");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("greater constructs a field w/o parameter name")
public void greaterCtor() {
Field<String> field = Field.greater("Great", "night");
assertEquals("Great", field.getName(), "Field name not filled correctly");
assertEquals(Op.GREATER, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals("night", field.getComparison().getValue(), "Field comparison value not filled correctly");
assertNull(field.getParameterName(), "The parameter name should have been null");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("greater constructs a field w/ parameter name")
public void greaterParameterCtor() {
Field<String> field = Field.greater("Great", "night", ":yeah");
assertEquals("Great", field.getName(), "Field name not filled correctly");
assertEquals(Op.GREATER, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals("night", field.getComparison().getValue(), "Field comparison value not filled correctly");
assertEquals(":yeah", field.getParameterName(), "Field parameter name not filled correctly");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("greaterOrEqual constructs a field w/o parameter name")
public void greaterOrEqualCtor() {
Field<Long> field = Field.greaterOrEqual("Nice", 88L);
assertEquals("Nice", field.getName(), "Field name not filled correctly");
assertEquals(Op.GREATER_OR_EQUAL, field.getComparison().getOp(),
"Field comparison operation not filled correctly");
assertEquals(88L, field.getComparison().getValue(), "Field comparison value not filled correctly");
assertNull(field.getParameterName(), "The parameter name should have been null");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("greaterOrEqual constructs a field w/ parameter name")
public void greaterOrEqualParameterCtor() {
Field<Long> field = Field.greaterOrEqual("Nice", 88L, ":nice");
assertEquals("Nice", field.getName(), "Field name not filled correctly");
assertEquals(Op.GREATER_OR_EQUAL, field.getComparison().getOp(),
"Field comparison operation not filled correctly");
assertEquals(88L, field.getComparison().getValue(), "Field comparison value not filled correctly");
assertEquals(":nice", field.getParameterName(), "Field parameter name not filled correctly");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("less constructs a field w/o parameter name")
public void lessCtor() {
Field<String> field = Field.less("Lesser", "seven");
assertEquals("Lesser", field.getName(), "Field name not filled correctly");
assertEquals(Op.LESS, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals("seven", field.getComparison().getValue(), "Field comparison value not filled correctly");
assertNull(field.getParameterName(), "The parameter name should have been null");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("less constructs a field w/ parameter name")
public void lessParameterCtor() {
Field<String> field = Field.less("Lesser", "seven", ":max");
assertEquals("Lesser", field.getName(), "Field name not filled correctly");
assertEquals(Op.LESS, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals("seven", field.getComparison().getValue(), "Field comparison value not filled correctly");
assertEquals(":max", field.getParameterName(), "Field parameter name not filled correctly");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("lessOrEqual constructs a field w/o parameter name")
public void lessOrEqualCtor() {
Field<String> field = Field.lessOrEqual("Nobody", "KNOWS");
assertEquals("Nobody", field.getName(), "Field name not filled correctly");
assertEquals(Op.LESS_OR_EQUAL, field.getComparison().getOp(),
"Field comparison operation not filled correctly");
assertEquals("KNOWS", field.getComparison().getValue(), "Field comparison value not filled correctly");
assertNull(field.getParameterName(), "The parameter name should have been null");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("lessOrEqual constructs a field w/ parameter name")
public void lessOrEqualParameterCtor() {
Field<String> field = Field.lessOrEqual("Nobody", "KNOWS", ":nope");
assertEquals("Nobody", field.getName(), "Field name not filled correctly");
assertEquals(Op.LESS_OR_EQUAL, field.getComparison().getOp(),
"Field comparison operation not filled correctly");
assertEquals("KNOWS", field.getComparison().getValue(), "Field comparison value not filled correctly");
assertEquals(":nope", field.getParameterName(), "Field parameter name not filled correctly");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("notEqual constructs a field w/o parameter name")
public void notEqualCtor() {
Field<String> field = Field.notEqual("Park", "here");
assertEquals("Park", field.getName(), "Field name not filled correctly");
assertEquals(Op.NOT_EQUAL, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals("here", field.getComparison().getValue(), "Field comparison value not filled correctly");
assertNull(field.getParameterName(), "The parameter name should have been null");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("notEqual constructs a field w/ parameter name")
public void notEqualParameterCtor() {
Field<String> field = Field.notEqual("Park", "here", ":now");
assertEquals("Park", field.getName(), "Field name not filled correctly");
assertEquals(Op.NOT_EQUAL, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals("here", field.getComparison().getValue(), "Field comparison value not filled correctly");
assertEquals(":now", field.getParameterName(), "Field parameter name not filled correctly");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("between constructs a field w/o parameter name")
public void betweenCtor() {
Field<Pair<Integer, Integer>> field = Field.between("Age", 18, 49);
assertEquals("Age", field.getName(), "Field name not filled correctly");
assertEquals(Op.BETWEEN, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals(18, field.getComparison().getValue().getFirst(),
"Field comparison min value not filled correctly");
assertEquals(49, field.getComparison().getValue().getSecond(),
"Field comparison max value not filled correctly");
assertNull(field.getParameterName(), "The parameter name should have been null");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("between constructs a field w/ parameter name")
public void betweenParameterCtor() {
Field<Pair<Integer, Integer>> field = Field.between("Age", 18, 49, ":limit");
assertEquals("Age", field.getName(), "Field name not filled correctly");
assertEquals(Op.BETWEEN, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals(18, field.getComparison().getValue().getFirst(),
"Field comparison min value not filled correctly");
assertEquals(49, field.getComparison().getValue().getSecond(),
"Field comparison max value not filled correctly");
assertEquals(":limit", field.getParameterName(), "The parameter name should have been null");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("any constructs a field w/o parameter name")
public void anyCtor() {
Field<Collection<Integer>> field = Field.any("Here", List.of(8, 16, 32));
assertEquals("Here", field.getName(), "Field name not filled correctly");
assertEquals(Op.IN, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals(List.of(8, 16, 32), field.getComparison().getValue(),
"Field comparison value not filled correctly");
assertNull(field.getParameterName(), "The parameter name should have been null");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("any constructs a field w/ parameter name")
public void anyParameterCtor() {
Field<Collection<Integer>> field = Field.any("Here", List.of(8, 16, 32), ":list");
assertEquals("Here", field.getName(), "Field name not filled correctly");
assertEquals(Op.IN, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals(List.of(8, 16, 32), field.getComparison().getValue(),
"Field comparison value not filled correctly");
assertEquals(":list", field.getParameterName(), "Field parameter name not filled correctly");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("inArray constructs a field w/o parameter name")
public void inArrayCtor() {
Field<Pair<String, Collection<String>>> field = Field.inArray("ArrayField", "table", List.of("z"));
assertEquals("ArrayField", field.getName(), "Field name not filled correctly");
assertEquals(Op.IN_ARRAY, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals("table", field.getComparison().getValue().getFirst(),
"Field comparison table not filled correctly");
assertEquals(List.of("z"), field.getComparison().getValue().getSecond(),
"Field comparison values not filled correctly");
assertNull(field.getParameterName(), "The parameter name should have been null");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("inArray constructs a field w/ parameter name")
public void inArrayParameterCtor() {
Field<Pair<String, Collection<String>>> field = Field.inArray("ArrayField", "table", List.of("z"), ":a");
assertEquals("ArrayField", field.getName(), "Field name not filled correctly");
assertEquals(Op.IN_ARRAY, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals("table", field.getComparison().getValue().getFirst(),
"Field comparison table not filled correctly");
assertEquals(List.of("z"), field.getComparison().getValue().getSecond(),
"Field comparison values not filled correctly");
assertEquals(":a", field.getParameterName(), "Field parameter name not filled correctly");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("exists constructs a field")
public void existsCtor() {
Field<String> field = Field.exists("Groovy");
assertEquals("Groovy", field.getName(), "Field name not filled correctly");
assertEquals(Op.EXISTS, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals("", field.getComparison().getValue(), "Field comparison value not filled correctly");
assertNull(field.getParameterName(), "The parameter name should have been null");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("notExists constructs a field")
public void notExistsCtor() {
Field<String> field = Field.notExists("Groovy");
assertEquals("Groovy", field.getName(), "Field name not filled correctly");
assertEquals(Op.NOT_EXISTS, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals("", field.getComparison().getValue(), "Field comparison value not filled correctly");
assertNull(field.getParameterName(), "The parameter name should have been null");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("named constructs a field")
public void namedCtor() {
Field<String> field = Field.named("Tacos");
assertEquals("Tacos", field.getName(), "Field name not filled correctly");
assertEquals(Op.EQUAL, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals("", field.getComparison().getValue(), "Field comparison value not filled correctly");
assertNull(field.getParameterName(), "The parameter name should have been null");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("static constructors fail for invalid parameter name")
public void staticCtorsFailOnParamName() {
assertThrows(DocumentException.class, () -> Field.equal("a", "b", "that ain't it, Jack..."));
}
@Test
@DisplayName("nameToPath creates a simple PostgreSQL SQL name")
public void nameToPathPostgresSimpleSQL() {
assertEquals("data->>'Simple'", Field.nameToPath("Simple", Dialect.POSTGRESQL, FieldFormat.SQL),
"Path not constructed correctly");
}
@Test
@DisplayName("nameToPath creates a simple SQLite SQL name")
public void nameToPathSQLiteSimpleSQL() {
assertEquals("data->>'Simple'", Field.nameToPath("Simple", Dialect.SQLITE, FieldFormat.SQL),
"Path not constructed correctly");
}
@Test
@DisplayName("nameToPath creates a nested PostgreSQL SQL name")
public void nameToPathPostgresNestedSQL() {
assertEquals("data#>>'{A,Long,Path,to,the,Property}'",
Field.nameToPath("A.Long.Path.to.the.Property", Dialect.POSTGRESQL, FieldFormat.SQL),
"Path not constructed correctly");
}
@Test
@DisplayName("nameToPath creates a nested SQLite SQL name")
public void nameToPathSQLiteNestedSQL() {
assertEquals("data->'A'->'Long'->'Path'->'to'->'the'->>'Property'",
Field.nameToPath("A.Long.Path.to.the.Property", Dialect.SQLITE, FieldFormat.SQL),
"Path not constructed correctly");
}
@Test
@DisplayName("nameToPath creates a simple PostgreSQL JSON name")
public void nameToPathPostgresSimpleJSON() {
assertEquals("data->'Simple'", Field.nameToPath("Simple", Dialect.POSTGRESQL, FieldFormat.JSON),
"Path not constructed correctly");
}
@Test
@DisplayName("nameToPath creates a simple SQLite JSON name")
public void nameToPathSQLiteSimpleJSON() {
assertEquals("data->'Simple'", Field.nameToPath("Simple", Dialect.SQLITE, FieldFormat.JSON),
"Path not constructed correctly");
}
@Test
@DisplayName("nameToPath creates a nested PostgreSQL JSON name")
public void nameToPathPostgresNestedJSON() {
assertEquals("data#>'{A,Long,Path,to,the,Property}'",
Field.nameToPath("A.Long.Path.to.the.Property", Dialect.POSTGRESQL, FieldFormat.JSON),
"Path not constructed correctly");
}
@Test
@DisplayName("nameToPath creates a nested SQLite JSON name")
public void nameToPathSQLiteNestedJSON() {
assertEquals("data->'A'->'Long'->'Path'->'to'->'the'->'Property'",
Field.nameToPath("A.Long.Path.to.the.Property", Dialect.SQLITE, FieldFormat.JSON),
"Path not constructed correctly");
}
}

View File

@@ -0,0 +1,80 @@
package solutions.bitbadger.documents.common.java;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import solutions.bitbadger.documents.common.Op;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* Unit tests for the `Op` enum
*/
@DisplayName("Java | Common | Op")
final public class OpTest {
@Test
@DisplayName("EQUAL uses proper SQL")
public void equalSQL() {
assertEquals("=", Op.EQUAL.getSql(), "The SQL for equal is incorrect");
}
@Test
@DisplayName("GREATER uses proper SQL")
public void greaterSQL() {
assertEquals(">", Op.GREATER.getSql(), "The SQL for greater is incorrect");
}
@Test
@DisplayName("GREATER_OR_EQUAL uses proper SQL")
public void greaterOrEqualSQL() {
assertEquals(">=", Op.GREATER_OR_EQUAL.getSql(), "The SQL for greater-or-equal is incorrect");
}
@Test
@DisplayName("LESS uses proper SQL")
public void lessSQL() {
assertEquals("<", Op.LESS.getSql(), "The SQL for less is incorrect");
}
@Test
@DisplayName("LESS_OR_EQUAL uses proper SQL")
public void lessOrEqualSQL() {
assertEquals("<=", Op.LESS_OR_EQUAL.getSql(), "The SQL for less-or-equal is incorrect");
}
@Test
@DisplayName("NOT_EQUAL uses proper SQL")
public void notEqualSQL() {
assertEquals("<>", Op.NOT_EQUAL.getSql(), "The SQL for not-equal is incorrect");
}
@Test
@DisplayName("BETWEEN uses proper SQL")
public void betweenSQL() {
assertEquals("BETWEEN", Op.BETWEEN.getSql(), "The SQL for between is incorrect");
}
@Test
@DisplayName("IN uses proper SQL")
public void inSQL() {
assertEquals("IN", Op.IN.getSql(), "The SQL for in is incorrect");
}
@Test
@DisplayName("IN_ARRAY uses proper SQL")
public void inArraySQL() {
assertEquals("??|", Op.IN_ARRAY.getSql(), "The SQL for in-array is incorrect");
}
@Test
@DisplayName("EXISTS uses proper SQL")
public void existsSQL() {
assertEquals("IS NOT NULL", Op.EXISTS.getSql(), "The SQL for exists is incorrect");
}
@Test
@DisplayName("NOT_EXISTS uses proper SQL")
public void notExistsSQL() {
assertEquals("IS NULL", Op.NOT_EXISTS.getSql(), "The SQL for not-exists is incorrect");
}
}

View File

@@ -0,0 +1,32 @@
package solutions.bitbadger.documents.common.java;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import solutions.bitbadger.documents.common.ParameterName;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* Unit tests for the `ParameterName` class
*/
@DisplayName("Java | Common | ParameterName")
final public class ParameterNameTest {
@Test
@DisplayName("derive works when given existing names")
public void withExisting() {
ParameterName names = new ParameterName();
assertEquals(":taco", names.derive(":taco"), "Name should have been :taco");
assertEquals(":field0", names.derive(null), "Counter should not have advanced for named field");
}
@Test
@DisplayName("derive works when given all anonymous fields")
public void allAnonymous() {
ParameterName names = new ParameterName();
assertEquals(":field0", names.derive(null), "Anonymous field name should have been returned");
assertEquals(":field1", names.derive(null), "Counter should have advanced from previous call");
assertEquals(":field2", names.derive(null), "Counter should have advanced from previous call");
assertEquals(":field3", names.derive(null), "Counter should have advanced from previous call");
}
}

View File

@@ -0,0 +1,40 @@
package solutions.bitbadger.documents.common.java;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import solutions.bitbadger.documents.common.DocumentException;
import solutions.bitbadger.documents.common.Parameter;
import solutions.bitbadger.documents.common.ParameterType;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for the `Parameter` class
*/
@DisplayName("Java | Common | Parameter")
final public class ParameterTest {
@Test
@DisplayName("Construction with colon-prefixed name")
public void ctorWithColon() {
Parameter<String> p = new Parameter<>(":test", ParameterType.STRING, "ABC");
assertEquals(":test", p.getName(), "Parameter name was incorrect");
assertEquals(ParameterType.STRING, p.getType(), "Parameter type was incorrect");
assertEquals("ABC", p.getValue(), "Parameter value was incorrect");
}
@Test
@DisplayName("Construction with at-sign-prefixed name")
public void ctorWithAtSign() {
Parameter<String> p = new Parameter<>("@yo", ParameterType.NUMBER, null);
assertEquals("@yo", p.getName(), "Parameter name was incorrect");
assertEquals(ParameterType.NUMBER, p.getType(), "Parameter type was incorrect");
assertNull(p.getValue(), "Parameter value was incorrect");
}
@Test
@DisplayName("Construction fails with incorrect prefix")
public void ctorFailsForPrefix() {
assertThrows(DocumentException.class, () -> new Parameter<>("it", ParameterType.JSON, ""));
}
}

View File

@@ -0,0 +1,165 @@
package solutions.bitbadger.documents.common
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.assertFalse
import kotlin.test.assertNotEquals
import kotlin.test.assertTrue
/**
* Unit tests for the `AutoId` enum
*/
@DisplayName("Kotlin | Common | AutoId")
class AutoIdTest {
@Test
@DisplayName("Generates a UUID string")
fun generateUUID() =
assertEquals(32, AutoId.generateUUID().length, "The UUID should have been a 32-character string")
@Test
@DisplayName("Generates a random hex character string of an even length")
fun generateRandomStringEven() {
val result = AutoId.generateRandomString(8)
assertEquals(8, result.length, "There should have been 8 characters in $result")
}
@Test
@DisplayName("Generates a random hex character string of an odd length")
fun generateRandomStringOdd() {
val result = AutoId.generateRandomString(11)
assertEquals(11, result.length, "There should have been 11 characters in $result")
}
@Test
@DisplayName("Generates different random hex character strings")
fun generateRandomStringIsRandom() {
val result1 = AutoId.generateRandomString(16)
val result2 = AutoId.generateRandomString(16)
assertNotEquals(result1, result2, "There should have been 2 different strings generated")
}
@Test
@DisplayName("needsAutoId fails for null document")
fun needsAutoIdFailsForNullDocument() {
assertThrows<DocumentException> { AutoId.needsAutoId(AutoId.DISABLED, null, "id") }
}
@Test
@DisplayName("needsAutoId fails for missing ID property")
fun needsAutoIdFailsForMissingId() {
assertThrows<DocumentException> { AutoId.needsAutoId(AutoId.UUID, IntIdClass(0), "Id") }
}
@Test
@DisplayName("needsAutoId returns false if disabled")
fun needsAutoIdFalseIfDisabled() =
assertFalse(AutoId.needsAutoId(AutoId.DISABLED, "", ""), "Disabled Auto ID should always return false")
@Test
@DisplayName("needsAutoId returns true for Number strategy and byte ID of 0")
fun needsAutoIdTrueForByteWithZero() =
assertTrue(AutoId.needsAutoId(AutoId.NUMBER, ByteIdClass(0), "id"), "Number Auto ID with 0 should return true")
@Test
@DisplayName("needsAutoId returns false for Number strategy and byte ID of non-0")
fun needsAutoIdFalseForByteWithNonZero() =
assertFalse(
AutoId.needsAutoId(AutoId.NUMBER, ByteIdClass(77), "id"),
"Number Auto ID with 77 should return false"
)
@Test
@DisplayName("needsAutoId returns true for Number strategy and short ID of 0")
fun needsAutoIdTrueForShortWithZero() =
assertTrue(AutoId.needsAutoId(AutoId.NUMBER, ShortIdClass(0), "id"), "Number Auto ID with 0 should return true")
@Test
@DisplayName("needsAutoId returns false for Number strategy and short ID of non-0")
fun needsAutoIdFalseForShortWithNonZero() =
assertFalse(
AutoId.needsAutoId(AutoId.NUMBER, ShortIdClass(31), "id"),
"Number Auto ID with 31 should return false"
)
@Test
@DisplayName("needsAutoId returns true for Number strategy and int ID of 0")
fun needsAutoIdTrueForIntWithZero() =
assertTrue(AutoId.needsAutoId(AutoId.NUMBER, IntIdClass(0), "id"), "Number Auto ID with 0 should return true")
@Test
@DisplayName("needsAutoId returns false for Number strategy and int ID of non-0")
fun needsAutoIdFalseForIntWithNonZero() =
assertFalse(AutoId.needsAutoId(AutoId.NUMBER, IntIdClass(6), "id"), "Number Auto ID with 6 should return false")
@Test
@DisplayName("needsAutoId returns true for Number strategy and long ID of 0")
fun needsAutoIdTrueForLongWithZero() =
assertTrue(AutoId.needsAutoId(AutoId.NUMBER, LongIdClass(0), "id"), "Number Auto ID with 0 should return true")
@Test
@DisplayName("needsAutoId returns false for Number strategy and long ID of non-0")
fun needsAutoIdFalseForLongWithNonZero() =
assertFalse(
AutoId.needsAutoId(AutoId.NUMBER, LongIdClass(2), "id"),
"Number Auto ID with 2 should return false"
)
@Test
@DisplayName("needsAutoId fails for Number strategy and non-number ID")
fun needsAutoIdFailsForNumberWithStringId() {
assertThrows<DocumentException> { AutoId.needsAutoId(AutoId.NUMBER, StringIdClass(""), "id") }
}
@Test
@DisplayName("needsAutoId returns true for UUID strategy and blank ID")
fun needsAutoIdTrueForUUIDWithBlank() =
assertTrue(
AutoId.needsAutoId(AutoId.UUID, StringIdClass(""), "id"),
"UUID Auto ID with blank should return true"
)
@Test
@DisplayName("needsAutoId returns false for UUID strategy and non-blank ID")
fun needsAutoIdFalseForUUIDNotBlank() =
assertFalse(
AutoId.needsAutoId(AutoId.UUID, StringIdClass("howdy"), "id"),
"UUID Auto ID with non-blank should return false"
)
@Test
@DisplayName("needsAutoId fails for UUID strategy and non-string ID")
fun needsAutoIdFailsForUUIDNonString() {
assertThrows<DocumentException> { AutoId.needsAutoId(AutoId.UUID, IntIdClass(5), "id") }
}
@Test
@DisplayName("needsAutoId returns true for Random String strategy and blank ID")
fun needsAutoIdTrueForRandomWithBlank() =
assertTrue(
AutoId.needsAutoId(AutoId.RANDOM_STRING, StringIdClass(""), "id"),
"Random String Auto ID with blank should return true"
)
@Test
@DisplayName("needsAutoId returns false for Random String strategy and non-blank ID")
fun needsAutoIdFalseForRandomNotBlank() =
assertFalse(
AutoId.needsAutoId(AutoId.RANDOM_STRING, StringIdClass("full"), "id"),
"Random String Auto ID with non-blank should return false"
)
@Test
@DisplayName("needsAutoId fails for Random String strategy and non-string ID")
fun needsAutoIdFailsForRandomNonString() {
assertThrows<DocumentException> { AutoId.needsAutoId(AutoId.RANDOM_STRING, ShortIdClass(55), "id") }
}
}
data class ByteIdClass(val id: Byte)
data class ShortIdClass(val id: Short)
data class IntIdClass(val id: Int)
data class LongIdClass(val id: Long)
data class StringIdClass(val id: String)

View File

@@ -0,0 +1,169 @@
package solutions.bitbadger.documents.common
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import solutions.bitbadger.documents.integration.TEST_TABLE
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
/**
* Unit tests for the `ComparisonBetween` class
*/
@DisplayName("ComparisonBetween")
class ComparisonBetweenTest {
@Test
@DisplayName("op is set to BETWEEN")
fun op() =
assertEquals(Op.BETWEEN, ComparisonBetween(Pair(0, 0)).op, "Between comparison should have BETWEEN op")
@Test
@DisplayName("isNumeric is false with strings")
fun isNumericFalseForStringsAndBetween() =
assertFalse(
ComparisonBetween(Pair("eh", "zed")).isNumeric,
"A BETWEEN with strings should not be numeric")
@Test
@DisplayName("isNumeric is true with bytes")
fun isNumericTrueForByteAndBetween() =
assertTrue(ComparisonBetween(Pair<Byte, Byte>(7, 11)).isNumeric, "A BETWEEN with bytes should be numeric")
@Test
@DisplayName("isNumeric is true with shorts")
fun isNumericTrueForShortAndBetween() =
assertTrue(
ComparisonBetween(Pair<Short, Short>(0, 9)).isNumeric,
"A BETWEEN with shorts should be numeric")
@Test
@DisplayName("isNumeric is true with ints")
fun isNumericTrueForIntAndBetween() =
assertTrue(ComparisonBetween(Pair(15, 44)).isNumeric, "A BETWEEN with ints should be numeric")
@Test
@DisplayName("isNumeric is true with longs")
fun isNumericTrueForLongAndBetween() =
assertTrue(ComparisonBetween(Pair(9L, 12L)).isNumeric, "A BETWEEN with longs should be numeric")
}
/**
* Unit tests for the `ComparisonIn` class
*/
@DisplayName("ComparisonIn")
class ComparisonInTest {
@Test
@DisplayName("op is set to IN")
fun op() =
assertEquals(Op.IN, ComparisonIn(listOf<String>()).op, "In comparison should have IN op")
@Test
@DisplayName("isNumeric is false for empty list of values")
fun isNumericFalseForEmptyList() =
assertFalse(ComparisonIn(listOf<Int>()).isNumeric, "An IN with empty list should not be numeric")
@Test
@DisplayName("isNumeric is false with strings")
fun isNumericFalseForStringsAndIn() =
assertFalse(ComparisonIn(listOf("a", "b", "c")).isNumeric, "An IN with strings should not be numeric")
@Test
@DisplayName("isNumeric is true with bytes")
fun isNumericTrueForByteAndIn() =
assertTrue(ComparisonIn(listOf<Byte>(4, 8)).isNumeric, "An IN with bytes should be numeric")
@Test
@DisplayName("isNumeric is true with shorts")
fun isNumericTrueForShortAndIn() =
assertTrue(ComparisonIn(listOf<Short>(18, 22)).isNumeric, "An IN with shorts should be numeric")
@Test
@DisplayName("isNumeric is true with ints")
fun isNumericTrueForIntAndIn() =
assertTrue(ComparisonIn(listOf(7, 8, 9)).isNumeric, "An IN with ints should be numeric")
@Test
@DisplayName("isNumeric is true with longs")
fun isNumericTrueForLongAndIn() =
assertTrue(ComparisonIn(listOf(3L)).isNumeric, "An IN with longs should be numeric")
}
/**
* Unit tests for the `ComparisonInArray` class
*/
@DisplayName("ComparisonInArray")
class ComparisonInArrayTest {
@Test
@DisplayName("op is set to IN_ARRAY")
fun op() =
assertEquals(
Op.IN_ARRAY,
ComparisonInArray(Pair(TEST_TABLE, listOf<String>())).op,
"InArray comparison should have IN_ARRAY op"
)
@Test
@DisplayName("isNumeric is false for empty list of values")
fun isNumericFalseForEmptyList() =
assertFalse(ComparisonIn(listOf<Int>()).isNumeric, "An IN_ARRAY with empty list should not be numeric")
@Test
@DisplayName("isNumeric is false with strings")
fun isNumericFalseForStringsAndIn() =
assertFalse(ComparisonIn(listOf("a", "b", "c")).isNumeric, "An IN_ARRAY with strings should not be numeric")
@Test
@DisplayName("isNumeric is false with bytes")
fun isNumericTrueForByteAndIn() =
assertTrue(ComparisonIn(listOf<Byte>(4, 8)).isNumeric, "An IN_ARRAY with bytes should not be numeric")
@Test
@DisplayName("isNumeric is false with shorts")
fun isNumericTrueForShortAndIn() =
assertTrue(ComparisonIn(listOf<Short>(18, 22)).isNumeric, "An IN_ARRAY with shorts should not be numeric")
@Test
@DisplayName("isNumeric is false with ints")
fun isNumericTrueForIntAndIn() =
assertTrue(ComparisonIn(listOf(7, 8, 9)).isNumeric, "An IN_ARRAY with ints should not be numeric")
@Test
@DisplayName("isNumeric is false with longs")
fun isNumericTrueForLongAndIn() =
assertTrue(ComparisonIn(listOf(3L)).isNumeric, "An IN_ARRAY with longs should not be numeric")
}
/**
* Unit tests for the `ComparisonSingle` class
*/
@DisplayName("ComparisonSingle")
class ComparisonSingleTest {
@Test
@DisplayName("isNumeric is false for string value")
fun isNumericFalseForString() =
assertFalse(ComparisonSingle(Op.EQUAL, "80").isNumeric, "A string should not be numeric")
@Test
@DisplayName("isNumeric is true for byte value")
fun isNumericTrueForByte() =
assertTrue(ComparisonSingle(Op.EQUAL, 47.toByte()).isNumeric, "A byte should be numeric")
@Test
@DisplayName("isNumeric is true for short value")
fun isNumericTrueForShort() =
assertTrue(ComparisonSingle(Op.EQUAL, 2.toShort()).isNumeric, "A short should be numeric")
@Test
@DisplayName("isNumeric is true for int value")
fun isNumericTrueForInt() =
assertTrue(ComparisonSingle(Op.EQUAL, 555).isNumeric, "An int should be numeric")
@Test
@DisplayName("isNumeric is true for long value")
fun isNumericTrueForLong() =
assertTrue(ComparisonSingle(Op.EQUAL, 82L).isNumeric, "A long should be numeric")
}

View File

@@ -0,0 +1,43 @@
package solutions.bitbadger.documents.common
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import kotlin.test.assertEquals
/**
* Unit tests for the `Configuration` object
*/
@DisplayName("Kotlin | Common | Configuration")
class ConfigurationTest {
@Test
@DisplayName("Default ID field is `id`")
fun defaultIdField() {
assertEquals("id", Configuration.idField, "Default ID field incorrect")
}
@Test
@DisplayName("Default Auto ID strategy is `DISABLED`")
fun defaultAutoId() {
assertEquals(AutoId.DISABLED, Configuration.autoIdStrategy, "Default Auto ID strategy should be `disabled`")
}
@Test
@DisplayName("Default ID string length should be 16")
fun defaultIdStringLength() {
assertEquals(16, Configuration.idStringLength, "Default ID string length should be 16")
}
@Test
@DisplayName("Dialect is derived from connection string")
fun dialectIsDerived() {
try {
assertThrows<DocumentException> { Configuration.dialect() }
Configuration.connectionString = "jdbc:postgresql:db"
assertEquals(Dialect.POSTGRESQL, Configuration.dialect())
} finally {
Configuration.connectionString = null
}
}
}

View File

@@ -0,0 +1,40 @@
package solutions.bitbadger.documents.common
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
/**
* Unit tests for the `Dialect` enum
*/
@DisplayName("Kotlin | Common | Dialect")
class DialectTest {
@Test
@DisplayName("deriveFromConnectionString derives PostgreSQL correctly")
fun derivesPostgres() =
assertEquals(
Dialect.POSTGRESQL, Dialect.deriveFromConnectionString("jdbc:postgresql:db"),
"Dialect should have been PostgreSQL")
@Test
@DisplayName("deriveFromConnectionString derives PostgreSQL correctly")
fun derivesSQLite() =
assertEquals(
Dialect.SQLITE, Dialect.deriveFromConnectionString("jdbc:sqlite:memory"),
"Dialect should have been SQLite")
@Test
@DisplayName("deriveFromConnectionString fails when the connection string is unknown")
fun deriveFailsWhenUnknown() {
try {
Dialect.deriveFromConnectionString("SQL Server")
} catch (ex: DocumentException) {
assertNotNull(ex.message, "The exception message should not have been null")
assertTrue(ex.message!!.contains("[SQL Server]"),
"The connection string should have been in the exception message")
}
}
}

View File

@@ -0,0 +1,24 @@
package solutions.bitbadger.documents.common
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
/**
* Unit tests for the `DocumentIndex` enum
*/
@DisplayName("Kotlin | Common | DocumentIndex")
class DocumentIndexTest {
@Test
@DisplayName("FULL uses proper SQL")
fun fullSQL() {
assertEquals("", DocumentIndex.FULL.sql, "The SQL for Full is incorrect")
}
@Test
@DisplayName("OPTIMIZED uses proper SQL")
fun optimizedSQL() {
assertEquals(" jsonb_path_ops", DocumentIndex.OPTIMIZED.sql, "The SQL for Optimized is incorrect")
}
}

View File

@@ -0,0 +1,24 @@
package solutions.bitbadger.documents.common
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
/**
* Unit tests for the `FieldMatch` enum
*/
@DisplayName("Kotlin | Common | FieldMatch")
class FieldMatchTest {
@Test
@DisplayName("ANY uses proper SQL")
fun any() {
assertEquals("OR", FieldMatch.ANY.sql, "ANY should use OR")
}
@Test
@DisplayName("ALL uses proper SQL")
fun all() {
assertEquals("AND", FieldMatch.ALL.sql, "ALL should use AND")
}
}

View File

@@ -0,0 +1,594 @@
package solutions.bitbadger.documents.common
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.assertNotSame
import kotlin.test.assertNull
/**
* Unit tests for the `Field` class
*/
@DisplayName("Kotlin | Common | Field")
class FieldTest {
/**
* Clear the connection string (resets Dialect)
*/
@AfterEach
fun cleanUp() {
Configuration.dialectValue = null
}
// ~~~ INSTANCE METHODS ~~~
@Test
@DisplayName("withParameterName fails for invalid name")
fun withParamNameFails() {
assertThrows<DocumentException> { Field.equal("it", "").withParameterName("2424") }
}
@Test
@DisplayName("withParameterName works with colon prefix")
fun withParamNameColon() {
val field = Field.equal("abc", "22").withQualifier("me")
val withParam = field.withParameterName(":test")
assertNotSame(field, withParam, "A new Field instance should have been created")
assertEquals(field.name, withParam.name, "Name should have been preserved")
assertEquals(field.comparison, withParam.comparison, "Comparison should have been preserved")
assertEquals(":test", withParam.parameterName, "Parameter name not set correctly")
assertEquals(field.qualifier, withParam.qualifier, "Qualifier should have been preserved")
}
@Test
@DisplayName("withParameterName works with at-sign prefix")
fun withParamNameAtSign() {
val field = Field.equal("def", "44")
val withParam = field.withParameterName("@unit")
assertNotSame(field, withParam, "A new Field instance should have been created")
assertEquals(field.name, withParam.name, "Name should have been preserved")
assertEquals(field.comparison, withParam.comparison, "Comparison should have been preserved")
assertEquals("@unit", withParam.parameterName, "Parameter name not set correctly")
assertEquals(field.qualifier, withParam.qualifier, "Qualifier should have been preserved")
}
@Test
@DisplayName("withQualifier sets qualifier correctly")
fun withQualifier() {
val field = Field.equal("j", "k")
val withQual = field.withQualifier("test")
assertNotSame(field, withQual, "A new Field instance should have been created")
assertEquals(field.name, withQual.name, "Name should have been preserved")
assertEquals(field.comparison, withQual.comparison, "Comparison should have been preserved")
assertEquals(field.parameterName, withQual.parameterName, "Parameter Name should have been preserved")
assertEquals("test", withQual.qualifier, "Qualifier not set correctly")
}
@Test
@DisplayName("path generates for simple unqualified PostgreSQL field")
fun pathPostgresSimpleUnqualified() =
assertEquals("data->>'SomethingCool'",
Field.greaterOrEqual("SomethingCool", 18).path(Dialect.POSTGRESQL, FieldFormat.SQL), "Path not correct")
@Test
@DisplayName("path generates for simple qualified PostgreSQL field")
fun pathPostgresSimpleQualified() =
assertEquals("this.data->>'SomethingElse'",
Field.less("SomethingElse", 9).withQualifier("this").path(Dialect.POSTGRESQL, FieldFormat.SQL),
"Path not correct")
@Test
@DisplayName("path generates for nested unqualified PostgreSQL field")
fun pathPostgresNestedUnqualified() {
assertEquals("data#>>'{My,Nested,Field}'",
Field.equal("My.Nested.Field", "howdy").path(Dialect.POSTGRESQL, FieldFormat.SQL), "Path not correct")
}
@Test
@DisplayName("path generates for nested qualified PostgreSQL field")
fun pathPostgresNestedQualified() =
assertEquals("bird.data#>>'{Nest,Away}'",
Field.equal("Nest.Away", "doc").withQualifier("bird").path(Dialect.POSTGRESQL, FieldFormat.SQL),
"Path not correct")
@Test
@DisplayName("path generates for simple unqualified SQLite field")
fun pathSQLiteSimpleUnqualified() =
assertEquals("data->>'SomethingCool'",
Field.greaterOrEqual("SomethingCool", 18).path(Dialect.SQLITE, FieldFormat.SQL), "Path not correct")
@Test
@DisplayName("path generates for simple qualified SQLite field")
fun pathSQLiteSimpleQualified() =
assertEquals("this.data->>'SomethingElse'",
Field.less("SomethingElse", 9).withQualifier("this").path(Dialect.SQLITE, FieldFormat.SQL),
"Path not correct")
@Test
@DisplayName("path generates for nested unqualified SQLite field")
fun pathSQLiteNestedUnqualified() =
assertEquals("data->'My'->'Nested'->>'Field'",
Field.equal("My.Nested.Field", "howdy").path(Dialect.SQLITE, FieldFormat.SQL), "Path not correct")
@Test
@DisplayName("path generates for nested qualified SQLite field")
fun pathSQLiteNestedQualified() =
assertEquals("bird.data->'Nest'->>'Away'",
Field.equal("Nest.Away", "doc").withQualifier("bird").path(Dialect.SQLITE, FieldFormat.SQL),
"Path not correct")
@Test
@DisplayName("toWhere generates for exists w/o qualifier (PostgreSQL)")
fun toWhereExistsNoQualPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
assertEquals("data->>'that_field' IS NOT NULL", Field.exists("that_field").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates for exists w/o qualifier (SQLite)")
fun toWhereExistsNoQualSQLite() {
Configuration.dialectValue = Dialect.SQLITE
assertEquals("data->>'that_field' IS NOT NULL", Field.exists("that_field").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates for not-exists w/o qualifier (PostgreSQL)")
fun toWhereNotExistsNoQualPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
assertEquals("data->>'a_field' IS NULL", Field.notExists("a_field").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates for not-exists w/o qualifier (SQLite)")
fun toWhereNotExistsNoQualSQLite() {
Configuration.dialectValue = Dialect.SQLITE
assertEquals("data->>'a_field' IS NULL", Field.notExists("a_field").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates for BETWEEN w/o qualifier, numeric range (PostgreSQL)")
fun toWhereBetweenNoQualNumericPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
assertEquals("(data->>'age')::numeric BETWEEN @agemin AND @agemax",
Field.between("age", 13, 17, "@age").toWhere(), "Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates for BETWEEN w/o qualifier, alphanumeric range (PostgreSQL)")
fun toWhereBetweenNoQualAlphaPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
assertEquals("data->>'city' BETWEEN :citymin AND :citymax",
Field.between("city", "Atlanta", "Chicago", ":city").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates for BETWEEN w/o qualifier (SQLite)")
fun toWhereBetweenNoQualSQLite() {
Configuration.dialectValue = Dialect.SQLITE
assertEquals("data->>'age' BETWEEN @agemin AND @agemax", Field.between("age", 13, 17, "@age").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates for BETWEEN w/ qualifier, numeric range (PostgreSQL)")
fun toWhereBetweenQualNumericPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
assertEquals("(test.data->>'age')::numeric BETWEEN @agemin AND @agemax",
Field.between("age", 13, 17, "@age").withQualifier("test").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates for BETWEEN w/ qualifier, alphanumeric range (PostgreSQL)")
fun toWhereBetweenQualAlphaPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
assertEquals("unit.data->>'city' BETWEEN :citymin AND :citymax",
Field.between("city", "Atlanta", "Chicago", ":city").withQualifier("unit").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates for BETWEEN w/ qualifier (SQLite)")
fun toWhereBetweenQualSQLite() {
Configuration.dialectValue = Dialect.SQLITE
assertEquals("my.data->>'age' BETWEEN @agemin AND @agemax",
Field.between("age", 13, 17, "@age").withQualifier("my").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates for IN/any, numeric values (PostgreSQL)")
fun toWhereAnyNumericPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
assertEquals("(data->>'even')::numeric IN (:nbr_0, :nbr_1, :nbr_2)",
Field.any("even", listOf(2, 4, 6), ":nbr").toWhere(), "Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates for IN/any, alphanumeric values (PostgreSQL)")
fun toWhereAnyAlphaPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
assertEquals("data->>'test' IN (:city_0, :city_1)",
Field.any("test", listOf("Atlanta", "Chicago"), ":city").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates for IN/any (SQLite)")
fun toWhereAnySQLite() {
Configuration.dialectValue = Dialect.SQLITE
assertEquals("data->>'test' IN (:city_0, :city_1)",
Field.any("test", listOf("Atlanta", "Chicago"), ":city").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates for inArray (PostgreSQL)")
fun toWhereInArrayPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
assertEquals("data->'even' ??| ARRAY[:it_0, :it_1, :it_2, :it_3]",
Field.inArray("even", "tbl", listOf(2, 4, 6, 8), ":it").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates for inArray (SQLite)")
fun toWhereInArraySQLite() {
Configuration.dialectValue = Dialect.SQLITE
assertEquals("EXISTS (SELECT 1 FROM json_each(tbl.data, '$.test') WHERE value IN (:city_0, :city_1))",
Field.inArray("test", "tbl", listOf("Atlanta", "Chicago"), ":city").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates for others w/o qualifier (PostgreSQL)")
fun toWhereOtherNoQualPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
assertEquals("data->>'some_field' = :value", Field.equal("some_field", "", ":value").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates for others w/o qualifier (SQLite)")
fun toWhereOtherNoQualSQLite() {
Configuration.dialectValue = Dialect.SQLITE
assertEquals("data->>'some_field' = :value", Field.equal("some_field", "", ":value").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates no-parameter w/ qualifier (PostgreSQL)")
fun toWhereNoParamWithQualPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
assertEquals("test.data->>'no_field' IS NOT NULL", Field.exists("no_field").withQualifier("test").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates no-parameter w/ qualifier (SQLite)")
fun toWhereNoParamWithQualSQLite() {
Configuration.dialectValue = Dialect.SQLITE
assertEquals("test.data->>'no_field' IS NOT NULL", Field.exists("no_field").withQualifier("test").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates parameter w/ qualifier (PostgreSQL)")
fun toWhereParamWithQualPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
assertEquals("(q.data->>'le_field')::numeric <= :it",
Field.lessOrEqual("le_field", 18, ":it").withQualifier("q").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates parameter w/ qualifier (SQLite)")
fun toWhereParamWithQualSQLite() {
Configuration.dialectValue = Dialect.SQLITE
assertEquals("q.data->>'le_field' <= :it",
Field.lessOrEqual("le_field", 18, ":it").withQualifier("q").toWhere(),
"Field WHERE clause not generated correctly")
}
// ~~~ COMPANION OBJECT TESTS ~~~
@Test
@DisplayName("equal constructs a field w/o parameter name")
fun equalCtor() {
val field = Field.equal("Test", 14)
assertEquals("Test", field.name, "Field name not filled correctly")
assertEquals(Op.EQUAL, field.comparison.op, "Field comparison operation not filled correctly")
assertEquals(14, field.comparison.value, "Field comparison value not filled correctly")
assertNull(field.parameterName, "The parameter name should have been null")
assertNull(field.qualifier, "The qualifier should have been null")
}
@Test
@DisplayName("equal constructs a field w/ parameter name")
fun equalParameterCtor() {
val field = Field.equal("Test", 14, ":w")
assertEquals("Test", field.name, "Field name not filled correctly")
assertEquals(Op.EQUAL, field.comparison.op, "Field comparison operation not filled correctly")
assertEquals(14, field.comparison.value, "Field comparison value not filled correctly")
assertEquals(":w", field.parameterName, "Field parameter name not filled correctly")
assertNull(field.qualifier, "The qualifier should have been null")
}
@Test
@DisplayName("greater constructs a field w/o parameter name")
fun greaterCtor() {
val field = Field.greater("Great", "night")
assertEquals("Great", field.name, "Field name not filled correctly")
assertEquals(Op.GREATER, field.comparison.op, "Field comparison operation not filled correctly")
assertEquals("night", field.comparison.value, "Field comparison value not filled correctly")
assertNull(field.parameterName, "The parameter name should have been null")
assertNull(field.qualifier, "The qualifier should have been null")
}
@Test
@DisplayName("greater constructs a field w/ parameter name")
fun greaterParameterCtor() {
val field = Field.greater("Great", "night", ":yeah")
assertEquals("Great", field.name, "Field name not filled correctly")
assertEquals(Op.GREATER, field.comparison.op, "Field comparison operation not filled correctly")
assertEquals("night", field.comparison.value, "Field comparison value not filled correctly")
assertEquals(":yeah", field.parameterName, "Field parameter name not filled correctly")
assertNull(field.qualifier, "The qualifier should have been null")
}
@Test
@DisplayName("greaterOrEqual constructs a field w/o parameter name")
fun greaterOrEqualCtor() {
val field = Field.greaterOrEqual("Nice", 88L)
assertEquals("Nice", field.name, "Field name not filled correctly")
assertEquals(Op.GREATER_OR_EQUAL, field.comparison.op, "Field comparison operation not filled correctly")
assertEquals(88L, field.comparison.value, "Field comparison value not filled correctly")
assertNull(field.parameterName, "The parameter name should have been null")
assertNull(field.qualifier, "The qualifier should have been null")
}
@Test
@DisplayName("greaterOrEqual constructs a field w/ parameter name")
fun greaterOrEqualParameterCtor() {
val field = Field.greaterOrEqual("Nice", 88L, ":nice")
assertEquals("Nice", field.name, "Field name not filled correctly")
assertEquals(Op.GREATER_OR_EQUAL, field.comparison.op, "Field comparison operation not filled correctly")
assertEquals(88L, field.comparison.value, "Field comparison value not filled correctly")
assertEquals(":nice", field.parameterName, "Field parameter name not filled correctly")
assertNull(field.qualifier, "The qualifier should have been null")
}
@Test
@DisplayName("less constructs a field w/o parameter name")
fun lessCtor() {
val field = Field.less("Lesser", "seven")
assertEquals("Lesser", field.name, "Field name not filled correctly")
assertEquals(Op.LESS, field.comparison.op, "Field comparison operation not filled correctly")
assertEquals("seven", field.comparison.value, "Field comparison value not filled correctly")
assertNull(field.parameterName, "The parameter name should have been null")
assertNull(field.qualifier, "The qualifier should have been null")
}
@Test
@DisplayName("less constructs a field w/ parameter name")
fun lessParameterCtor() {
val field = Field.less("Lesser", "seven", ":max")
assertEquals("Lesser", field.name, "Field name not filled correctly")
assertEquals(Op.LESS, field.comparison.op, "Field comparison operation not filled correctly")
assertEquals("seven", field.comparison.value, "Field comparison value not filled correctly")
assertEquals(":max", field.parameterName, "Field parameter name not filled correctly")
assertNull(field.qualifier, "The qualifier should have been null")
}
@Test
@DisplayName("lessOrEqual constructs a field w/o parameter name")
fun lessOrEqualCtor() {
val field = Field.lessOrEqual("Nobody", "KNOWS")
assertEquals("Nobody", field.name, "Field name not filled correctly")
assertEquals(Op.LESS_OR_EQUAL, field.comparison.op, "Field comparison operation not filled correctly")
assertEquals("KNOWS", field.comparison.value, "Field comparison value not filled correctly")
assertNull(field.parameterName, "The parameter name should have been null")
assertNull(field.qualifier, "The qualifier should have been null")
}
@Test
@DisplayName("lessOrEqual constructs a field w/ parameter name")
fun lessOrEqualParameterCtor() {
val field = Field.lessOrEqual("Nobody", "KNOWS", ":nope")
assertEquals("Nobody", field.name, "Field name not filled correctly")
assertEquals(Op.LESS_OR_EQUAL, field.comparison.op, "Field comparison operation not filled correctly")
assertEquals("KNOWS", field.comparison.value, "Field comparison value not filled correctly")
assertEquals(":nope", field.parameterName, "Field parameter name not filled correctly")
assertNull(field.qualifier, "The qualifier should have been null")
}
@Test
@DisplayName("notEqual constructs a field w/o parameter name")
fun notEqualCtor() {
val field = Field.notEqual("Park", "here")
assertEquals("Park", field.name, "Field name not filled correctly")
assertEquals(Op.NOT_EQUAL, field.comparison.op, "Field comparison operation not filled correctly")
assertEquals("here", field.comparison.value, "Field comparison value not filled correctly")
assertNull(field.parameterName, "The parameter name should have been null")
assertNull(field.qualifier, "The qualifier should have been null")
}
@Test
@DisplayName("notEqual constructs a field w/ parameter name")
fun notEqualParameterCtor() {
val field = Field.notEqual("Park", "here", ":now")
assertEquals("Park", field.name, "Field name not filled correctly")
assertEquals(Op.NOT_EQUAL, field.comparison.op, "Field comparison operation not filled correctly")
assertEquals("here", field.comparison.value, "Field comparison value not filled correctly")
assertEquals(":now", field.parameterName, "Field parameter name not filled correctly")
assertNull(field.qualifier, "The qualifier should have been null")
}
@Test
@DisplayName("between constructs a field w/o parameter name")
fun betweenCtor() {
val field = Field.between("Age", 18, 49)
assertEquals("Age", field.name, "Field name not filled correctly")
assertEquals(Op.BETWEEN, field.comparison.op, "Field comparison operation not filled correctly")
assertEquals(18, field.comparison.value.first, "Field comparison min value not filled correctly")
assertEquals(49, field.comparison.value.second, "Field comparison max value not filled correctly")
assertNull(field.parameterName, "The parameter name should have been null")
assertNull(field.qualifier, "The qualifier should have been null")
}
@Test
@DisplayName("between constructs a field w/ parameter name")
fun betweenParameterCtor() {
val field = Field.between("Age", 18, 49, ":limit")
assertEquals("Age", field.name, "Field name not filled correctly")
assertEquals(Op.BETWEEN, field.comparison.op, "Field comparison operation not filled correctly")
assertEquals(18, field.comparison.value.first, "Field comparison min value not filled correctly")
assertEquals(49, field.comparison.value.second, "Field comparison max value not filled correctly")
assertEquals(":limit", field.parameterName, "The parameter name should have been null")
assertNull(field.qualifier, "The qualifier should have been null")
}
@Test
@DisplayName("any constructs a field w/o parameter name")
fun anyCtor() {
val field = Field.any("Here", listOf(8, 16, 32))
assertEquals("Here", field.name, "Field name not filled correctly")
assertEquals(Op.IN, field.comparison.op, "Field comparison operation not filled correctly")
assertEquals(listOf(8, 16, 32), field.comparison.value, "Field comparison value not filled correctly")
assertNull(field.parameterName, "The parameter name should have been null")
assertNull(field.qualifier, "The qualifier should have been null")
}
@Test
@DisplayName("any constructs a field w/ parameter name")
fun anyParameterCtor() {
val field = Field.any("Here", listOf(8, 16, 32), ":list")
assertEquals("Here", field.name, "Field name not filled correctly")
assertEquals(Op.IN, field.comparison.op, "Field comparison operation not filled correctly")
assertEquals(listOf(8, 16, 32), field.comparison.value, "Field comparison value not filled correctly")
assertEquals(":list", field.parameterName, "Field parameter name not filled correctly")
assertNull(field.qualifier, "The qualifier should have been null")
}
@Test
@DisplayName("inArray constructs a field w/o parameter name")
fun inArrayCtor() {
val field = Field.inArray("ArrayField", "table", listOf("z"))
assertEquals("ArrayField", field.name, "Field name not filled correctly")
assertEquals(Op.IN_ARRAY, field.comparison.op, "Field comparison operation not filled correctly")
assertEquals("table", field.comparison.value.first, "Field comparison table not filled correctly")
assertEquals(listOf("z"), field.comparison.value.second, "Field comparison values not filled correctly")
assertNull(field.parameterName, "The parameter name should have been null")
assertNull(field.qualifier, "The qualifier should have been null")
}
@Test
@DisplayName("inArray constructs a field w/ parameter name")
fun inArrayParameterCtor() {
val field = Field.inArray("ArrayField", "table", listOf("z"), ":a")
assertEquals("ArrayField", field.name, "Field name not filled correctly")
assertEquals(Op.IN_ARRAY, field.comparison.op, "Field comparison operation not filled correctly")
assertEquals("table", field.comparison.value.first, "Field comparison table not filled correctly")
assertEquals(listOf("z"), field.comparison.value.second, "Field comparison values not filled correctly")
assertEquals(":a", field.parameterName, "Field parameter name not filled correctly")
assertNull(field.qualifier, "The qualifier should have been null")
}
@Test
@DisplayName("exists constructs a field")
fun existsCtor() {
val field = Field.exists("Groovy")
assertEquals("Groovy", field.name, "Field name not filled correctly")
assertEquals(Op.EXISTS, field.comparison.op, "Field comparison operation not filled correctly")
assertEquals("", field.comparison.value, "Field comparison value not filled correctly")
assertNull(field.parameterName, "The parameter name should have been null")
assertNull(field.qualifier, "The qualifier should have been null")
}
@Test
@DisplayName("notExists constructs a field")
fun notExistsCtor() {
val field = Field.notExists("Groovy")
assertEquals("Groovy", field.name, "Field name not filled correctly")
assertEquals(Op.NOT_EXISTS, field.comparison.op, "Field comparison operation not filled correctly")
assertEquals("", field.comparison.value, "Field comparison value not filled correctly")
assertNull(field.parameterName, "The parameter name should have been null")
assertNull(field.qualifier, "The qualifier should have been null")
}
@Test
@DisplayName("named constructs a field")
fun namedCtor() {
val field = Field.named("Tacos")
assertEquals("Tacos", field.name, "Field name not filled correctly")
assertEquals(Op.EQUAL, field.comparison.op, "Field comparison operation not filled correctly")
assertEquals("", field.comparison.value, "Field comparison value not filled correctly")
assertNull(field.parameterName, "The parameter name should have been null")
assertNull(field.qualifier, "The qualifier should have been null")
}
@Test
@DisplayName("static constructors fail for invalid parameter name")
fun staticCtorsFailOnParamName() {
assertThrows<DocumentException> { Field.equal("a", "b", "that ain't it, Jack...") }
}
@Test
@DisplayName("nameToPath creates a simple PostgreSQL SQL name")
fun nameToPathPostgresSimpleSQL() =
assertEquals("data->>'Simple'", Field.nameToPath("Simple", Dialect.POSTGRESQL, FieldFormat.SQL),
"Path not constructed correctly")
@Test
@DisplayName("nameToPath creates a simple SQLite SQL name")
fun nameToPathSQLiteSimpleSQL() =
assertEquals("data->>'Simple'", Field.nameToPath("Simple", Dialect.SQLITE, FieldFormat.SQL),
"Path not constructed correctly")
@Test
@DisplayName("nameToPath creates a nested PostgreSQL SQL name")
fun nameToPathPostgresNestedSQL() =
assertEquals("data#>>'{A,Long,Path,to,the,Property}'",
Field.nameToPath("A.Long.Path.to.the.Property", Dialect.POSTGRESQL, FieldFormat.SQL),
"Path not constructed correctly")
@Test
@DisplayName("nameToPath creates a nested SQLite SQL name")
fun nameToPathSQLiteNestedSQL() =
assertEquals("data->'A'->'Long'->'Path'->'to'->'the'->>'Property'",
Field.nameToPath("A.Long.Path.to.the.Property", Dialect.SQLITE, FieldFormat.SQL),
"Path not constructed correctly")
@Test
@DisplayName("nameToPath creates a simple PostgreSQL JSON name")
fun nameToPathPostgresSimpleJSON() =
assertEquals("data->'Simple'", Field.nameToPath("Simple", Dialect.POSTGRESQL, FieldFormat.JSON),
"Path not constructed correctly")
@Test
@DisplayName("nameToPath creates a simple SQLite JSON name")
fun nameToPathSQLiteSimpleJSON() =
assertEquals("data->'Simple'", Field.nameToPath("Simple", Dialect.SQLITE, FieldFormat.JSON),
"Path not constructed correctly")
@Test
@DisplayName("nameToPath creates a nested PostgreSQL JSON name")
fun nameToPathPostgresNestedJSON() =
assertEquals("data#>'{A,Long,Path,to,the,Property}'",
Field.nameToPath("A.Long.Path.to.the.Property", Dialect.POSTGRESQL, FieldFormat.JSON),
"Path not constructed correctly")
@Test
@DisplayName("nameToPath creates a nested SQLite JSON name")
fun nameToPathSQLiteNestedJSON() =
assertEquals("data->'A'->'Long'->'Path'->'to'->'the'->'Property'",
Field.nameToPath("A.Long.Path.to.the.Property", Dialect.SQLITE, FieldFormat.JSON),
"Path not constructed correctly")
}

View File

@@ -0,0 +1,78 @@
package solutions.bitbadger.documents.common
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
/**
* Unit tests for the `Op` enum
*/
@DisplayName("Kotlin | Common | Op")
class OpTest {
@Test
@DisplayName("EQUAL uses proper SQL")
fun equalSQL() {
assertEquals("=", Op.EQUAL.sql, "The SQL for equal is incorrect")
}
@Test
@DisplayName("GREATER uses proper SQL")
fun greaterSQL() {
assertEquals(">", Op.GREATER.sql, "The SQL for greater is incorrect")
}
@Test
@DisplayName("GREATER_OR_EQUAL uses proper SQL")
fun greaterOrEqualSQL() {
assertEquals(">=", Op.GREATER_OR_EQUAL.sql, "The SQL for greater-or-equal is incorrect")
}
@Test
@DisplayName("LESS uses proper SQL")
fun lessSQL() {
assertEquals("<", Op.LESS.sql, "The SQL for less is incorrect")
}
@Test
@DisplayName("LESS_OR_EQUAL uses proper SQL")
fun lessOrEqualSQL() {
assertEquals("<=", Op.LESS_OR_EQUAL.sql, "The SQL for less-or-equal is incorrect")
}
@Test
@DisplayName("NOT_EQUAL uses proper SQL")
fun notEqualSQL() {
assertEquals("<>", Op.NOT_EQUAL.sql, "The SQL for not-equal is incorrect")
}
@Test
@DisplayName("BETWEEN uses proper SQL")
fun betweenSQL() {
assertEquals("BETWEEN", Op.BETWEEN.sql, "The SQL for between is incorrect")
}
@Test
@DisplayName("IN uses proper SQL")
fun inSQL() {
assertEquals("IN", Op.IN.sql, "The SQL for in is incorrect")
}
@Test
@DisplayName("IN_ARRAY uses proper SQL")
fun inArraySQL() {
assertEquals("??|", Op.IN_ARRAY.sql, "The SQL for in-array is incorrect")
}
@Test
@DisplayName("EXISTS uses proper SQL")
fun existsSQL() {
assertEquals("IS NOT NULL", Op.EXISTS.sql, "The SQL for exists is incorrect")
}
@Test
@DisplayName("NOT_EXISTS uses proper SQL")
fun notExistsSQL() {
assertEquals("IS NULL", Op.NOT_EXISTS.sql, "The SQL for not-exists is incorrect")
}
}

View File

@@ -0,0 +1,30 @@
package solutions.bitbadger.documents.common
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
/**
* Unit tests for the `ParameterName` class
*/
@DisplayName("Kotlin | Common | ParameterName")
class ParameterNameTest {
@Test
@DisplayName("derive works when given existing names")
fun withExisting() {
val names = ParameterName()
assertEquals(":taco", names.derive(":taco"), "Name should have been :taco")
assertEquals(":field0", names.derive(null), "Counter should not have advanced for named field")
}
@Test
@DisplayName("derive works when given all anonymous fields")
fun allAnonymous() {
val names = ParameterName()
assertEquals(":field0", names.derive(null), "Anonymous field name should have been returned")
assertEquals(":field1", names.derive(null), "Counter should have advanced from previous call")
assertEquals(":field2", names.derive(null), "Counter should have advanced from previous call")
assertEquals(":field3", names.derive(null), "Counter should have advanced from previous call")
}
}

View File

@@ -0,0 +1,38 @@
package solutions.bitbadger.documents.common
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.assertThrows
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
/**
* Unit tests for the `Parameter` class
*/
@DisplayName("Kotlin | Common | Parameter")
class ParameterTest {
@Test
@DisplayName("Construction with colon-prefixed name")
fun ctorWithColon() {
val p = Parameter(":test", ParameterType.STRING, "ABC")
assertEquals(":test", p.name, "Parameter name was incorrect")
assertEquals(ParameterType.STRING, p.type, "Parameter type was incorrect")
assertEquals("ABC", p.value, "Parameter value was incorrect")
}
@Test
@DisplayName("Construction with at-sign-prefixed name")
fun ctorWithAtSign() {
val p = Parameter("@yo", ParameterType.NUMBER, null)
assertEquals("@yo", p.name, "Parameter name was incorrect")
assertEquals(ParameterType.NUMBER, p.type, "Parameter type was incorrect")
assertNull(p.value, "Parameter value was incorrect")
}
@Test
@DisplayName("Construction fails with incorrect prefix")
fun ctorFailsForPrefix() {
assertThrows<DocumentException> { Parameter("it", ParameterType.JSON, "") }
}
}

View File

@@ -0,0 +1,90 @@
package solutions.bitbadger.documents.common.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.common.Configuration
import solutions.bitbadger.documents.common.Dialect
import solutions.bitbadger.documents.common.DocumentException
import solutions.bitbadger.documents.common.Field
import kotlin.test.assertEquals
/**
* Unit tests for the `Count` object
*/
@DisplayName("Kotlin | Common | Query: Count")
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) }
}
}

View File

@@ -0,0 +1,134 @@
package solutions.bitbadger.documents.common.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.common.Configuration
import solutions.bitbadger.documents.common.Dialect
import solutions.bitbadger.documents.common.DocumentException
import solutions.bitbadger.documents.common.DocumentIndex
import kotlin.test.assertEquals
/**
* Unit tests for the `Definition` object
*/
@DisplayName("Kotlin | Common | Query: Definition")
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"
)
@Test
@DisplayName("ensureDocumentIndexOn generates Full for PostgreSQL")
fun ensureDocumentIndexOnFullPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
assertEquals(
"CREATE INDEX IF NOT EXISTS idx_${tbl}_document ON $tbl USING GIN (data)",
Definition.ensureDocumentIndexOn(tbl, DocumentIndex.FULL),
"CREATE INDEX for full document index incorrect"
)
}
@Test
@DisplayName("ensureDocumentIndexOn generates Optimized for PostgreSQL")
fun ensureDocumentIndexOnOptimizedPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
assertEquals(
"CREATE INDEX IF NOT EXISTS idx_${tbl}_document ON $tbl USING GIN (data jsonb_path_ops)",
Definition.ensureDocumentIndexOn(tbl, DocumentIndex.OPTIMIZED),
"CREATE INDEX for optimized document index incorrect"
)
}
@Test
@DisplayName("ensureDocumentIndexOn fails for SQLite")
fun ensureDocumentIndexOnFailsSQLite() {
Configuration.dialectValue = Dialect.SQLITE
assertThrows<DocumentException> { Definition.ensureDocumentIndexOn(tbl, DocumentIndex.FULL) }
}
}

View File

@@ -0,0 +1,102 @@
package solutions.bitbadger.documents.common.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.common.Configuration
import solutions.bitbadger.documents.common.Dialect
import solutions.bitbadger.documents.common.DocumentException
import solutions.bitbadger.documents.common.Field
import kotlin.test.assertEquals
/**
* Unit tests for the `Delete` object
*/
@DisplayName("Kotlin | Common | Query: Delete")
class DeleteTest {
/** 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(
"DELETE FROM $tbl WHERE data->>'id' = :id",
Delete.byId<String>(tbl), "Delete query not constructed correctly"
)
}
@Test
@DisplayName("byId generates correctly (SQLite)")
fun byIdSQLite() {
Configuration.dialectValue = Dialect.SQLITE
assertEquals(
"DELETE FROM $tbl WHERE data->>'id' = :id",
Delete.byId<String>(tbl), "Delete query not constructed correctly"
)
}
@Test
@DisplayName("byFields generates correctly (PostgreSQL)")
fun byFieldsPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
assertEquals(
"DELETE FROM $tbl WHERE data->>'a' = :b", Delete.byFields(tbl, listOf(Field.equal("a", "", ":b"))),
"Delete query not constructed correctly"
)
}
@Test
@DisplayName("byFields generates correctly (SQLite)")
fun byFieldsSQLite() {
Configuration.dialectValue = Dialect.SQLITE
assertEquals(
"DELETE FROM $tbl WHERE data->>'a' = :b", Delete.byFields(tbl, listOf(Field.equal("a", "", ":b"))),
"Delete query not constructed correctly"
)
}
@Test
@DisplayName("byContains generates correctly (PostgreSQL)")
fun byContainsPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
assertEquals(
"DELETE FROM $tbl WHERE data @> :criteria", Delete.byContains(tbl), "Delete query not constructed correctly"
)
}
@Test
@DisplayName("byContains fails (SQLite)")
fun byContainsSQLite() {
Configuration.dialectValue = Dialect.SQLITE
assertThrows<DocumentException> { Delete.byContains(tbl) }
}
@Test
@DisplayName("byJsonPath generates correctly (PostgreSQL)")
fun byJsonPathPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
assertEquals(
"DELETE FROM $tbl WHERE jsonb_path_exists(data, :path::jsonpath)", Delete.byJsonPath(tbl),
"Delete query not constructed correctly"
)
}
@Test
@DisplayName("byJsonPath fails (SQLite)")
fun byJsonPathSQLite() {
Configuration.dialectValue = Dialect.SQLITE
assertThrows<DocumentException> { Delete.byJsonPath(tbl) }
}
}

View File

@@ -0,0 +1,150 @@
package solutions.bitbadger.documents.common.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.common.AutoId
import solutions.bitbadger.documents.common.Configuration
import solutions.bitbadger.documents.common.Dialect
import solutions.bitbadger.documents.common.DocumentException
import kotlin.test.assertEquals
import kotlin.test.assertTrue
/**
* Unit tests for the `Document` object
*/
@DisplayName("Kotlin | Common | Query: Document")
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")
}

View File

@@ -0,0 +1,105 @@
package solutions.bitbadger.documents.common.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.common.Configuration
import solutions.bitbadger.documents.common.Dialect
import solutions.bitbadger.documents.common.DocumentException
import solutions.bitbadger.documents.common.Field
import kotlin.test.assertEquals
/**
* Unit tests for the `Exists` object
*/
@DisplayName("Kotlin | Common | Query: Exists")
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) }
}
}

View File

@@ -0,0 +1,110 @@
package solutions.bitbadger.documents.common.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.common.Configuration
import solutions.bitbadger.documents.common.Dialect
import solutions.bitbadger.documents.common.DocumentException
import solutions.bitbadger.documents.common.Field
import kotlin.test.assertEquals
/**
* Unit tests for the `Find` object
*/
@DisplayName("Kotlin | Common | Query: Find")
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) }
}
}

View File

@@ -0,0 +1,105 @@
package solutions.bitbadger.documents.common.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.common.Configuration
import solutions.bitbadger.documents.common.Dialect
import solutions.bitbadger.documents.common.DocumentException
import solutions.bitbadger.documents.common.Field
import kotlin.test.assertEquals
/**
* Unit tests for the `Patch` object
*/
@DisplayName("Kotlin | Common | Query: Patch")
class PatchTest {
/** Test table name */
private val tbl = "test_table"
/**
* Reset the dialect
*/
@AfterEach
fun cleanUp() {
Configuration.dialectValue = null
}
@Test
@DisplayName("byId generates correctly (PostgreSQL)")
fun byIdPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
assertEquals(
"UPDATE $tbl SET data = data || :data WHERE data->>'id' = :id",
Patch.byId<String>(tbl), "Patch query not constructed correctly"
)
}
@Test
@DisplayName("byId generates correctly (SQLite)")
fun byIdSQLite() {
Configuration.dialectValue = Dialect.SQLITE
assertEquals(
"UPDATE $tbl SET data = json_patch(data, json(:data)) WHERE data->>'id' = :id",
Patch.byId<String>(tbl), "Patch query not constructed correctly"
)
}
@Test
@DisplayName("byFields generates correctly (PostgreSQL)")
fun byFieldsPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
assertEquals(
"UPDATE $tbl SET data = data || :data WHERE data->>'z' = :y",
Patch.byFields(tbl, listOf(Field.equal("z", "", ":y"))),
"Patch query not constructed correctly"
)
}
@Test
@DisplayName("byFields generates correctly (SQLite)")
fun byFieldsSQLite() {
Configuration.dialectValue = Dialect.SQLITE
assertEquals(
"UPDATE $tbl SET data = json_patch(data, json(:data)) WHERE data->>'z' = :y",
Patch.byFields(tbl, listOf(Field.equal("z", "", ":y"))),
"Patch query not constructed correctly"
)
}
@Test
@DisplayName("byContains generates correctly (PostgreSQL)")
fun byContainsPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
assertEquals(
"UPDATE $tbl SET data = data || :data WHERE data @> :criteria", Patch.byContains(tbl),
"Patch query not constructed correctly"
)
}
@Test
@DisplayName("byContains fails (SQLite)")
fun byContainsSQLite() {
Configuration.dialectValue = Dialect.SQLITE
assertThrows<DocumentException> { Patch.byContains(tbl) }
}
@Test
@DisplayName("byJsonPath generates correctly (PostgreSQL)")
fun byJsonPathPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
assertEquals(
"UPDATE $tbl SET data = data || :data WHERE jsonb_path_exists(data, :path::jsonpath)",
Patch.byJsonPath(tbl), "Patch query not constructed correctly"
)
}
@Test
@DisplayName("byJsonPath fails (SQLite)")
fun byJsonPathSQLite() {
Configuration.dialectValue = Dialect.SQLITE
assertThrows<DocumentException> { Patch.byJsonPath(tbl) }
}
}

View File

@@ -0,0 +1,178 @@
package solutions.bitbadger.documents.common.query
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import solutions.bitbadger.documents.common.Configuration
import solutions.bitbadger.documents.common.Dialect
import solutions.bitbadger.documents.common.Field
import solutions.bitbadger.documents.common.FieldMatch
import kotlin.test.assertEquals
/**
* Unit tests for the top-level query functions
*/
@DisplayName("Kotlin | Common | 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"
)
}

View File

@@ -0,0 +1,110 @@
package solutions.bitbadger.documents.common.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 solutions.bitbadger.documents.common.Configuration
import solutions.bitbadger.documents.common.Dialect
import solutions.bitbadger.documents.common.DocumentException
import solutions.bitbadger.documents.common.Field
import kotlin.test.assertEquals
/**
* Unit tests for the `RemoveFields` object
*/
@DisplayName("Kotlin | Common | Query: RemoveFields")
class RemoveFieldsTest {
/** Test table name */
private val tbl = "test_table"
/**
* Reset the dialect
*/
@AfterEach
fun cleanUp() {
Configuration.dialectValue = null
}
@Test
@DisplayName("byId generates correctly (PostgreSQL)")
fun byIdPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
assertEquals(
"UPDATE $tbl SET data = data - :name::text[] WHERE data->>'id' = :id",
RemoveFields.byId<String>(tbl, Parameters.fieldNames(listOf("a", "z"))),
"Remove Fields query not constructed correctly"
)
}
@Test
@DisplayName("byId generates correctly (SQLite)")
fun byIdSQLite() {
Configuration.dialectValue = Dialect.SQLITE
assertEquals(
"UPDATE $tbl SET data = json_remove(data, :name0, :name1) WHERE data->>'id' = :id",
RemoveFields.byId<String>(tbl, Parameters.fieldNames(listOf("a", "z"))),
"Remove Field query not constructed correctly"
)
}
@Test
@DisplayName("byFields generates correctly (PostgreSQL)")
fun byFieldsPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
assertEquals(
"UPDATE $tbl SET data = data - :name::text[] WHERE data->>'f' > :g",
RemoveFields.byFields(tbl, Parameters.fieldNames(listOf("b", "c")), listOf(Field.greater("f", "", ":g"))),
"Remove Field query not constructed correctly"
)
}
@Test
@DisplayName("byFields generates correctly (SQLite)")
fun byFieldsSQLite() {
Configuration.dialectValue = Dialect.SQLITE
assertEquals(
"UPDATE $tbl SET data = json_remove(data, :name0, :name1) WHERE data->>'f' > :g",
RemoveFields.byFields(tbl, Parameters.fieldNames(listOf("b", "c")), listOf(Field.greater("f", "", ":g"))),
"Remove Field query not constructed correctly"
)
}
@Test
@DisplayName("byContains generates correctly (PostgreSQL)")
fun byContainsPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
assertEquals(
"UPDATE $tbl SET data = data - :name::text[] WHERE data @> :criteria",
RemoveFields.byContains(tbl, Parameters.fieldNames(listOf("m", "n"))),
"Remove Field query not constructed correctly"
)
}
@Test
@DisplayName("byContains fails (SQLite)")
fun byContainsSQLite() {
Configuration.dialectValue = Dialect.SQLITE
assertThrows<DocumentException> { RemoveFields.byContains(tbl, listOf()) }
}
@Test
@DisplayName("byJsonPath generates correctly (PostgreSQL)")
fun byJsonPathPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
assertEquals(
"UPDATE $tbl SET data = data - :name::text[] WHERE jsonb_path_exists(data, :path::jsonpath)",
RemoveFields.byJsonPath(tbl, Parameters.fieldNames(listOf("o", "p"))),
"Remove Field query not constructed correctly"
)
}
@Test
@DisplayName("byJsonPath fails (SQLite)")
fun byJsonPathSQLite() {
Configuration.dialectValue = Dialect.SQLITE
assertThrows<DocumentException> { RemoveFields.byJsonPath(tbl, listOf()) }
}
}

View File

@@ -0,0 +1,176 @@
package solutions.bitbadger.documents.common.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.common.*
import kotlin.test.assertEquals
/**
* Unit tests for the `Where` object
*/
@DisplayName("Kotlin | Common | Query: Where")
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() }
}
}