Initial Development #1

Merged
danieljsummers merged 88 commits from v1-rc into main 2025-04-16 01:29:20 +00:00
11 changed files with 646 additions and 199 deletions
Showing only changes of commit 623e49243d - Show all commits

View File

@ -45,15 +45,15 @@ enum class AutoId {
* @param document The document whose need of an automatic ID should be determined * @param document The document whose need of an automatic ID should be determined
* @param idProp The name of the document property containing the ID * @param idProp The name of the document property containing the ID
* @return `true` if the document needs an automatic ID, `false` if not * @return `true` if the document needs an automatic ID, `false` if not
* @throws IllegalArgumentException If bad input prevents the determination * @throws DocumentException If bad input prevents the determination
*/ */
fun <T> needsAutoId(strategy: AutoId, document: T, idProp: String): Boolean { fun <T> needsAutoId(strategy: AutoId, document: T, idProp: String): Boolean {
if (document == null) throw IllegalArgumentException("document cannot be null") if (document == null) throw DocumentException("document cannot be null")
if (strategy == DISABLED) return false; if (strategy == DISABLED) return false;
val id = document!!::class.memberProperties.find { it.name == idProp } val id = document!!::class.memberProperties.find { it.name == idProp }
if (id == null) throw IllegalArgumentException("$idProp not found in document") if (id == null) throw DocumentException("$idProp not found in document")
if (strategy == NUMBER) { if (strategy == NUMBER) {
return when (id.returnType) { return when (id.returnType) {
@ -61,7 +61,7 @@ enum class AutoId {
Short::class.createType() -> id.call(document) == 0.toShort() Short::class.createType() -> id.call(document) == 0.toShort()
Int::class.createType() -> id.call(document) == 0 Int::class.createType() -> id.call(document) == 0
Long::class.createType() -> id.call(document) == 0.toLong() Long::class.createType() -> id.call(document) == 0.toLong()
else -> throw IllegalArgumentException("$idProp was not a number; cannot auto-generate number ID") else -> throw DocumentException("$idProp was not a number; cannot auto-generate number ID")
} }
} }
@ -69,7 +69,7 @@ enum class AutoId {
return id.call(document) == "" return id.call(document) == ""
} }
throw IllegalArgumentException("$idProp was not a string; cannot auto-generate UUID or random string") throw DocumentException("$idProp was not a string; cannot auto-generate UUID or random string")
} }
} }
} }

View File

@ -10,16 +10,15 @@ class Comparison<T>(val op: Op, val value: T) {
/** Is the value for this comparison a numeric value? */ /** Is the value for this comparison a numeric value? */
val isNumeric: Boolean val isNumeric: Boolean
get() = get() {
if (op == Op.IN || op == Op.BETWEEN) { val toCheck = when (op) {
Op.IN -> {
val values = value as? Collection<*> val values = value as? Collection<*>
if (values.isNullOrEmpty()) { if (values.isNullOrEmpty()) "" else values.elementAt(0)
false
} else {
val first = values.elementAt(0)
first is Byte || first is Short || first is Int || first is Long
} }
} else { Op.BETWEEN -> (value as Pair<*, *>).first
value is Byte || value is Short || value is Int || value is Long else -> value
}
return toCheck is Byte || toCheck is Short || toCheck is Int || toCheck is Long
} }
} }

View File

@ -20,8 +20,8 @@ enum class Dialect {
*/ */
fun deriveFromConnectionString(connectionString: String): Dialect = fun deriveFromConnectionString(connectionString: String): Dialect =
when { when {
connectionString.contains("sqlite") -> SQLITE connectionString.contains(":sqlite:") -> SQLITE
connectionString.contains("postgresql") -> POSTGRESQL connectionString.contains(":postgresql:") -> POSTGRESQL
else -> throw DocumentException("Cannot determine dialect from [$connectionString]") else -> throw DocumentException("Cannot determine dialect from [$connectionString]")
} }
} }

View File

@ -20,8 +20,11 @@ class Field<T> private constructor(
* @param paramName The parameter name to use for this field * @param paramName The parameter name to use for this field
* @return A new `Field` with the parameter name specified * @return A new `Field` with the parameter name specified
*/ */
fun withParameterName(paramName: String): Field<T> = fun withParameterName(paramName: String): Field<T> {
Field(name, comparison, paramName, qualifier) if (!paramName.startsWith(':') && !paramName.startsWith('@'))
throw DocumentException("Parameter must start with : or @ ($paramName)")
return Field(name, comparison, paramName, qualifier)
}
/** /**
* Specify a qualifier (alias) for the document table * Specify a qualifier (alias) for the document table
@ -29,7 +32,7 @@ class Field<T> private constructor(
* @param alias The table alias for this field * @param alias The table alias for this field
* @return A new `Field` with the table qualifier specified * @return A new `Field` with the table qualifier specified
*/ */
fun withQualifier(alias: String): Field<T> = fun withQualifier(alias: String) =
Field(name, comparison, parameterName, alias) Field(name, comparison, parameterName, alias)
/** /**
@ -74,16 +77,16 @@ class Field<T> private constructor(
in listOf(Op.EXISTS, Op.NOT_EXISTS) -> "" in listOf(Op.EXISTS, Op.NOT_EXISTS) -> ""
Op.BETWEEN -> " ${parameterName}min AND ${parameterName}max" Op.BETWEEN -> " ${parameterName}min AND ${parameterName}max"
Op.IN -> " ($inParameterNames)" Op.IN -> " ($inParameterNames)"
Op.IN_ARRAY -> if (dialect == Dialect.POSTGRESQL) " ARRAY['$inParameterNames']" else "" Op.IN_ARRAY -> if (dialect == Dialect.POSTGRESQL) " ARRAY[$inParameterNames]" else ""
else -> " $parameterName" else -> " $parameterName"
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
return if (dialect == Dialect.SQLITE && comparison.op == Op.IN_ARRAY) { return if (dialect == Dialect.SQLITE && comparison.op == Op.IN_ARRAY) {
val (table, _) = comparison.value as? Pair<String, *> ?: throw DocumentException("InArray field invalid") 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)" "EXISTS (SELECT 1 FROM json_each($table.data, '$.$name') WHERE value IN ($inParameterNames))"
} else { } else {
"$fieldPath ${comparison.op.sql} $criteria" "$fieldPath ${comparison.op.sql}$criteria"
} }
} }
@ -94,60 +97,66 @@ class Field<T> private constructor(
* *
* @param name The name of the field to be compared * @param name The name of the field to be compared
* @param value The value for the comparison * @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 * @return A `Field` with the given comparison
*/ */
fun <T> equal(name: String, value: T) = fun <T> equal(name: String, value: T, paramName: String? = null) =
Field(name, Comparison(Op.EQUAL, value)) Field(name, Comparison(Op.EQUAL, value), paramName)
/** /**
* Create a field greater-than comparison * Create a field greater-than comparison
* *
* @param name The name of the field to be compared * @param name The name of the field to be compared
* @param value The value for the comparison * @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 * @return A `Field` with the given comparison
*/ */
fun <T> greater(name: String, value: T) = fun <T> greater(name: String, value: T, paramName: String? = null) =
Field(name, Comparison(Op.GREATER, value)) Field(name, Comparison(Op.GREATER, value), paramName)
/** /**
* Create a field greater-than-or-equal-to comparison * Create a field greater-than-or-equal-to comparison
* *
* @param name The name of the field to be compared * @param name The name of the field to be compared
* @param value The value for the comparison * @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 * @return A `Field` with the given comparison
*/ */
fun <T> greaterOrEqual(name: String, value: T) = fun <T> greaterOrEqual(name: String, value: T, paramName: String? = null) =
Field(name, Comparison(Op.GREATER_OR_EQUAL, value)) Field(name, Comparison(Op.GREATER_OR_EQUAL, value), paramName)
/** /**
* Create a field less-than comparison * Create a field less-than comparison
* *
* @param name The name of the field to be compared * @param name The name of the field to be compared
* @param value The value for the comparison * @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 * @return A `Field` with the given comparison
*/ */
fun <T> less(name: String, value: T) = fun <T> less(name: String, value: T, paramName: String? = null) =
Field(name, Comparison(Op.LESS, value)) Field(name, Comparison(Op.LESS, value), paramName)
/** /**
* Create a field less-than-or-equal-to comparison * Create a field less-than-or-equal-to comparison
* *
* @param name The name of the field to be compared * @param name The name of the field to be compared
* @param value The value for the comparison * @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 * @return A `Field` with the given comparison
*/ */
fun <T> lessOrEqual(name: String, value: T) = fun <T> lessOrEqual(name: String, value: T, paramName: String? = null) =
Field(name, Comparison(Op.LESS_OR_EQUAL, value)) Field(name, Comparison(Op.LESS_OR_EQUAL, value), paramName)
/** /**
* Create a field inequality comparison * Create a field inequality comparison
* *
* @param name The name of the field to be compared * @param name The name of the field to be compared
* @param value The value for the comparison * @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 * @return A `Field` with the given comparison
*/ */
fun <T> notEqual(name: String, value: T) = fun <T> notEqual(name: String, value: T, paramName: String? = null) =
Field(name, Comparison(Op.NOT_EQUAL, value)) Field(name, Comparison(Op.NOT_EQUAL, value), paramName)
/** /**
* Create a field range comparison * Create a field range comparison
@ -155,20 +164,22 @@ class Field<T> private constructor(
* @param name The name of the field to be compared * @param name The name of the field to be compared
* @param minValue The lower value for the comparison * @param minValue The lower value for the comparison
* @param maxValue The upper 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 * @return A `Field` with the given comparison
*/ */
fun <T> between(name: String, minValue: T, maxValue: T) = fun <T> between(name: String, minValue: T, maxValue: T, paramName: String? = null) =
Field(name, Comparison(Op.BETWEEN, Pair(minValue, maxValue))) Field(name, Comparison(Op.BETWEEN, Pair(minValue, maxValue)), paramName)
/** /**
* Create a field where any values match (SQL `IN`) * Create a field where any values match (SQL `IN`)
* *
* @param name The name of the field to be compared * @param name The name of the field to be compared
* @param values The values for the comparison * @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 * @return A `Field` with the given comparison
*/ */
fun <T> any(name: String, values: List<T>) = fun <T> any(name: String, values: Collection<T>, paramName: String? = null) =
Field(name, Comparison(Op.IN, values)) Field(name, Comparison(Op.IN, values), paramName)
/** /**
* Create a field where values should exist in a document's array * Create a field where values should exist in a document's array
@ -176,10 +187,11 @@ class Field<T> private constructor(
* @param name The name of the field to be compared * @param name The name of the field to be compared
* @param tableName The name of the document table * @param tableName The name of the document table
* @param values The values for the comparison * @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 * @return A `Field` with the given comparison
*/ */
fun <T> inArray(name: String, tableName: String, values: List<T>) = fun <T> inArray(name: String, tableName: String, values: Collection<T>, paramName: String? = null) =
Field(name, Comparison(Op.IN_ARRAY, Pair(tableName, values))) Field(name, Comparison(Op.IN_ARRAY, Pair(tableName, values)), paramName)
fun exists(name: String) = fun exists(name: String) =
Field(name, Comparison(Op.EXISTS, "")) Field(name, Comparison(Op.EXISTS, ""))

View File

@ -34,7 +34,7 @@ object Query {
* @param docId The ID value (optional; used for type determinations, string assumed if not provided) * @param docId The ID value (optional; used for type determinations, string assumed if not provided)
*/ */
fun <TKey> byId(parameterName: String = ":id", docId: TKey? = null) = fun <TKey> byId(parameterName: String = ":id", docId: TKey? = null) =
byFields(listOf(Field.equal(Configuration.idField, docId ?: "").withParameterName(parameterName))) byFields(listOf(Field.equal(Configuration.idField, docId ?: "", parameterName)))
/** /**
* Create a `WHERE` clause fragment to implement a JSON containment query (PostgreSQL only) * Create a `WHERE` clause fragment to implement a JSON containment query (PostgreSQL only)

View File

@ -12,9 +12,8 @@ class AutoIdTest {
@Test @Test
@DisplayName("Generates a UUID string") @DisplayName("Generates a UUID string")
fun generateUUID() { fun generateUUID() =
assertEquals(32, AutoId.generateUUID().length, "The UUID should have been a 32-character string") assertEquals(32, AutoId.generateUUID().length, "The UUID should have been a 32-character string")
}
@Test @Test
@DisplayName("Generates a random hex character string of an even length") @DisplayName("Generates a random hex character string of an even length")
@ -41,120 +40,107 @@ class AutoIdTest {
@Test @Test
@DisplayName("needsAutoId fails for null document") @DisplayName("needsAutoId fails for null document")
fun needsAutoIdFailsForNullDocument() { fun needsAutoIdFailsForNullDocument() {
assertThrows<IllegalArgumentException> { AutoId.needsAutoId(AutoId.DISABLED, null, "id") } assertThrows<DocumentException> { AutoId.needsAutoId(AutoId.DISABLED, null, "id") }
} }
@Test @Test
@DisplayName("needsAutoId fails for missing ID property") @DisplayName("needsAutoId fails for missing ID property")
fun needsAutoIdFailsForMissingId() { fun needsAutoIdFailsForMissingId() {
assertThrows<IllegalArgumentException> { AutoId.needsAutoId(AutoId.UUID, IntIdClass(0), "Id") } assertThrows<DocumentException> { AutoId.needsAutoId(AutoId.UUID, IntIdClass(0), "Id") }
} }
@Test @Test
@DisplayName("needsAutoId returns false if disabled") @DisplayName("needsAutoId returns false if disabled")
fun needsAutoIdFalseIfDisabled() { fun needsAutoIdFalseIfDisabled() =
assertFalse(AutoId.needsAutoId(AutoId.DISABLED, "", ""), "Disabled Auto ID should always return false") assertFalse(AutoId.needsAutoId(AutoId.DISABLED, "", ""), "Disabled Auto ID should always return false")
}
@Test @Test
@DisplayName("needsAutoId returns true for Number strategy and byte ID of 0") @DisplayName("needsAutoId returns true for Number strategy and byte ID of 0")
fun needsAutoIdTrueForByteWithZero() { fun needsAutoIdTrueForByteWithZero() =
assertTrue(AutoId.needsAutoId(AutoId.NUMBER, ByteIdClass(0), "id"), "Number Auto ID with 0 should return true") assertTrue(AutoId.needsAutoId(AutoId.NUMBER, ByteIdClass(0), "id"), "Number Auto ID with 0 should return true")
}
@Test @Test
@DisplayName("needsAutoId returns false for Number strategy and byte ID of non-0") @DisplayName("needsAutoId returns false for Number strategy and byte ID of non-0")
fun needsAutoIdFalseForByteWithNonZero() { fun needsAutoIdFalseForByteWithNonZero() =
assertFalse(AutoId.needsAutoId(AutoId.NUMBER, ByteIdClass(77), "id"), assertFalse(AutoId.needsAutoId(AutoId.NUMBER, ByteIdClass(77), "id"),
"Number Auto ID with 77 should return false") "Number Auto ID with 77 should return false")
}
@Test @Test
@DisplayName("needsAutoId returns true for Number strategy and short ID of 0") @DisplayName("needsAutoId returns true for Number strategy and short ID of 0")
fun needsAutoIdTrueForShortWithZero() { fun needsAutoIdTrueForShortWithZero() =
assertTrue(AutoId.needsAutoId(AutoId.NUMBER, ShortIdClass(0), "id"), "Number Auto ID with 0 should return true") assertTrue(AutoId.needsAutoId(AutoId.NUMBER, ShortIdClass(0), "id"), "Number Auto ID with 0 should return true")
}
@Test @Test
@DisplayName("needsAutoId returns false for Number strategy and short ID of non-0") @DisplayName("needsAutoId returns false for Number strategy and short ID of non-0")
fun needsAutoIdFalseForShortWithNonZero() { fun needsAutoIdFalseForShortWithNonZero() =
assertFalse(AutoId.needsAutoId(AutoId.NUMBER, ShortIdClass(31), "id"), assertFalse(AutoId.needsAutoId(AutoId.NUMBER, ShortIdClass(31), "id"),
"Number Auto ID with 31 should return false") "Number Auto ID with 31 should return false")
}
@Test @Test
@DisplayName("needsAutoId returns true for Number strategy and int ID of 0") @DisplayName("needsAutoId returns true for Number strategy and int ID of 0")
fun needsAutoIdTrueForIntWithZero() { fun needsAutoIdTrueForIntWithZero() =
assertTrue(AutoId.needsAutoId(AutoId.NUMBER, IntIdClass(0), "id"), "Number Auto ID with 0 should return true") assertTrue(AutoId.needsAutoId(AutoId.NUMBER, IntIdClass(0), "id"), "Number Auto ID with 0 should return true")
}
@Test @Test
@DisplayName("needsAutoId returns false for Number strategy and int ID of non-0") @DisplayName("needsAutoId returns false for Number strategy and int ID of non-0")
fun needsAutoIdFalseForIntWithNonZero() { fun needsAutoIdFalseForIntWithNonZero() =
assertFalse(AutoId.needsAutoId(AutoId.NUMBER, IntIdClass(6), "id"), "Number Auto ID with 6 should return false") assertFalse(AutoId.needsAutoId(AutoId.NUMBER, IntIdClass(6), "id"), "Number Auto ID with 6 should return false")
}
@Test @Test
@DisplayName("needsAutoId returns true for Number strategy and long ID of 0") @DisplayName("needsAutoId returns true for Number strategy and long ID of 0")
fun needsAutoIdTrueForLongWithZero() { fun needsAutoIdTrueForLongWithZero() =
assertTrue(AutoId.needsAutoId(AutoId.NUMBER, LongIdClass(0), "id"), "Number Auto ID with 0 should return true") assertTrue(AutoId.needsAutoId(AutoId.NUMBER, LongIdClass(0), "id"), "Number Auto ID with 0 should return true")
}
@Test @Test
@DisplayName("needsAutoId returns false for Number strategy and long ID of non-0") @DisplayName("needsAutoId returns false for Number strategy and long ID of non-0")
fun needsAutoIdFalseForLongWithNonZero() { fun needsAutoIdFalseForLongWithNonZero() =
assertFalse(AutoId.needsAutoId(AutoId.NUMBER, IntIdClass(2), "id"), "Number Auto ID with 2 should return false") assertFalse(AutoId.needsAutoId(AutoId.NUMBER, IntIdClass(2), "id"), "Number Auto ID with 2 should return false")
}
@Test @Test
@DisplayName("needsAutoId fails for Number strategy and non-number ID") @DisplayName("needsAutoId fails for Number strategy and non-number ID")
fun needsAutoIdFailsForNumberWithStringId() { fun needsAutoIdFailsForNumberWithStringId() {
assertThrows<IllegalArgumentException> { AutoId.needsAutoId(AutoId.NUMBER, StringIdClass(""), "id") } assertThrows<DocumentException> { AutoId.needsAutoId(AutoId.NUMBER, StringIdClass(""), "id") }
} }
@Test @Test
@DisplayName("needsAutoId returns true for UUID strategy and blank ID") @DisplayName("needsAutoId returns true for UUID strategy and blank ID")
fun needsAutoIdTrueForUUIDWithBlank() { fun needsAutoIdTrueForUUIDWithBlank() =
assertTrue(AutoId.needsAutoId(AutoId.UUID, StringIdClass(""), "id"), assertTrue(AutoId.needsAutoId(AutoId.UUID, StringIdClass(""), "id"),
"UUID Auto ID with blank should return true") "UUID Auto ID with blank should return true")
}
@Test @Test
@DisplayName("needsAutoId returns false for UUID strategy and non-blank ID") @DisplayName("needsAutoId returns false for UUID strategy and non-blank ID")
fun needsAutoIdFalseForUUIDNotBlank() { fun needsAutoIdFalseForUUIDNotBlank() =
assertFalse(AutoId.needsAutoId(AutoId.UUID, StringIdClass("howdy"), "id"), assertFalse(AutoId.needsAutoId(AutoId.UUID, StringIdClass("howdy"), "id"),
"UUID Auto ID with non-blank should return false") "UUID Auto ID with non-blank should return false")
}
@Test @Test
@DisplayName("needsAutoId fails for UUID strategy and non-string ID") @DisplayName("needsAutoId fails for UUID strategy and non-string ID")
fun needsAutoIdFailsForUUIDNonString() { fun needsAutoIdFailsForUUIDNonString() {
assertThrows<IllegalArgumentException> { AutoId.needsAutoId(AutoId.UUID, IntIdClass(5), "id") } assertThrows<DocumentException> { AutoId.needsAutoId(AutoId.UUID, IntIdClass(5), "id") }
} }
@Test @Test
@DisplayName("needsAutoId returns true for Random String strategy and blank ID") @DisplayName("needsAutoId returns true for Random String strategy and blank ID")
fun needsAutoIdTrueForRandomWithBlank() { fun needsAutoIdTrueForRandomWithBlank() =
assertTrue(AutoId.needsAutoId(AutoId.RANDOM_STRING, StringIdClass(""), "id"), assertTrue(AutoId.needsAutoId(AutoId.RANDOM_STRING, StringIdClass(""), "id"),
"Random String Auto ID with blank should return true") "Random String Auto ID with blank should return true")
}
@Test @Test
@DisplayName("needsAutoId returns false for Random String strategy and non-blank ID") @DisplayName("needsAutoId returns false for Random String strategy and non-blank ID")
fun needsAutoIdFalseForRandomNotBlank() { fun needsAutoIdFalseForRandomNotBlank() =
assertFalse(AutoId.needsAutoId(AutoId.RANDOM_STRING, StringIdClass("full"), "id"), assertFalse(AutoId.needsAutoId(AutoId.RANDOM_STRING, StringIdClass("full"), "id"),
"Random String Auto ID with non-blank should return false") "Random String Auto ID with non-blank should return false")
}
@Test @Test
@DisplayName("needsAutoId fails for Random String strategy and non-string ID") @DisplayName("needsAutoId fails for Random String strategy and non-string ID")
fun needsAutoIdFailsForRandomNonString() { fun needsAutoIdFailsForRandomNonString() {
assertThrows<IllegalArgumentException> { AutoId.needsAutoId(AutoId.RANDOM_STRING, ShortIdClass(55), "id") } assertThrows<DocumentException> { AutoId.needsAutoId(AutoId.RANDOM_STRING, ShortIdClass(55), "id") }
} }
} }
data class ByteIdClass(var id: Byte) data class ByteIdClass(val id: Byte)
data class ShortIdClass(var id: Short) data class ShortIdClass(val id: Short)
data class IntIdClass(var id: Int) data class IntIdClass(val id: Int)
data class LongIdClass(var id: Long) data class LongIdClass(val id: Long)
data class StringIdClass(var id: String) data class StringIdClass(val id: String)

View File

@ -0,0 +1,91 @@
package solutions.bitbadger.documents
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class ComparisonTest {
@Test
@DisplayName("isNumeric is false for empty list of values")
fun isNumericFalseForEmptyList() =
assertFalse(Comparison(Op.IN, listOf<Int>()).isNumeric, "An IN with empty list should not be numeric")
@Test
@DisplayName("isNumeric is false for IN with strings")
fun isNumericFalseForStringsAndIn() =
assertFalse(Comparison(Op.IN, listOf("a", "b", "c")).isNumeric, "An IN with strings should not be numeric")
@Test
@DisplayName("isNumeric is true for IN with bytes")
fun isNumericTrueForByteAndIn() =
assertTrue(Comparison(Op.IN, listOf<Byte>(4, 8)).isNumeric, "An IN with bytes should be numeric")
@Test
@DisplayName("isNumeric is true for IN with shorts")
fun isNumericTrueForShortAndIn() =
assertTrue(Comparison(Op.IN, listOf<Short>(18, 22)).isNumeric, "An IN with shorts should be numeric")
@Test
@DisplayName("isNumeric is true for IN with ints")
fun isNumericTrueForIntAndIn() =
assertTrue(Comparison(Op.IN, listOf(7, 8, 9)).isNumeric, "An IN with ints should be numeric")
@Test
@DisplayName("isNumeric is true for IN with longs")
fun isNumericTrueForLongAndIn() =
assertTrue(Comparison(Op.IN, listOf(3L)).isNumeric, "An IN with longs should be numeric")
@Test
@DisplayName("isNumeric is false for BETWEEN with strings")
fun isNumericFalseForStringsAndBetween() =
assertFalse(Comparison(Op.BETWEEN, Pair("eh", "zed")).isNumeric,
"A BETWEEN with strings should not be numeric")
@Test
@DisplayName("isNumeric is true for BETWEEN with bytes")
fun isNumericTrueForByteAndBetween() =
assertTrue(Comparison(Op.BETWEEN, Pair<Byte, Byte>(7, 11)).isNumeric, "A BETWEEN with bytes should be numeric")
@Test
@DisplayName("isNumeric is true for BETWEEN with shorts")
fun isNumericTrueForShortAndBetween() =
assertTrue(Comparison(Op.BETWEEN, Pair<Short, Short>(0, 9)).isNumeric,
"A BETWEEN with shorts should be numeric")
@Test
@DisplayName("isNumeric is true for BETWEEN with ints")
fun isNumericTrueForIntAndBetween() =
assertTrue(Comparison(Op.BETWEEN, Pair(15, 44)).isNumeric, "A BETWEEN with ints should be numeric")
@Test
@DisplayName("isNumeric is true for BETWEEN with longs")
fun isNumericTrueForLongAndBetween() =
assertTrue(Comparison(Op.BETWEEN, Pair(9L, 12L)).isNumeric, "A BETWEEN with longs should be numeric")
@Test
@DisplayName("isNumeric is false for string value")
fun isNumericFalseForString() =
assertFalse(Comparison(Op.EQUAL, "80").isNumeric, "A string should not be numeric")
@Test
@DisplayName("isNumeric is true for byte value")
fun isNumericTrueForByte() =
assertTrue(Comparison(Op.EQUAL, 47.toByte()).isNumeric, "A byte should be numeric")
@Test
@DisplayName("isNumeric is true for short value")
fun isNumericTrueForShort() =
assertTrue(Comparison(Op.EQUAL, 2.toShort()).isNumeric, "A short should be numeric")
@Test
@DisplayName("isNumeric is true for int value")
fun isNumericTrueForInt() =
assertTrue(Comparison(Op.EQUAL, 555).isNumeric, "An int should be numeric")
@Test
@DisplayName("isNumeric is true for long value")
fun isNumericTrueForLong() =
assertTrue(Comparison(Op.EQUAL, 82L).isNumeric, "A long should be numeric")
}

View File

@ -2,10 +2,20 @@ package solutions.bitbadger.documents
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class ConfigurationTest { class ConfigurationTest {
@Test
@DisplayName("Default JSON options are as expected")
fun defaultJsonOptions() {
assertTrue(Configuration.json.configuration.encodeDefaults, "Encode Defaults should have been set")
assertFalse(Configuration.json.configuration.explicitNulls, "Explicit Nulls should not have been set")
}
@Test @Test
@DisplayName("Default ID field is `id`") @DisplayName("Default ID field is `id`")
fun defaultIdField() { fun defaultIdField() {
@ -23,4 +33,16 @@ class ConfigurationTest {
fun defaultIdStringLength() { fun defaultIdStringLength() {
assertEquals(16, Configuration.idStringLength, "Default ID string length should be 16") 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,34 @@
package solutions.bitbadger.documents
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
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

@ -1,14 +1,302 @@
package solutions.bitbadger.documents package solutions.bitbadger.documents
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertNotSame
import kotlin.test.assertNull import kotlin.test.assertNull
class FieldTest { class FieldTest {
/**
* Clear the connection string (resets Dialect)
*/
@AfterEach
fun cleanUp() {
Configuration.connectionString = null
}
// ~~~ INSTANCE METHODS ~~~
@Test @Test
@DisplayName("equal constructs a field") @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.connectionString = ":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.connectionString = ":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.connectionString = ":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.connectionString = ":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.connectionString = ":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.connectionString = ":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.connectionString = ":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.connectionString = ":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.connectionString = ":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.connectionString = ":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.connectionString = ":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.connectionString = ":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.connectionString = ":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.connectionString = ":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.connectionString = ":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.connectionString = ":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.connectionString = ":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.connectionString = ":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.connectionString = ":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.connectionString = ":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.connectionString = ":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() { fun equalCtor() {
val field = Field.equal("Test", 14) val field = Field.equal("Test", 14)
assertEquals("Test", field.name, "Field name not filled correctly") assertEquals("Test", field.name, "Field name not filled correctly")
@ -19,7 +307,18 @@ class FieldTest {
} }
@Test @Test
@DisplayName("greater constructs a field") @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() { fun greaterCtor() {
val field = Field.greater("Great", "night") val field = Field.greater("Great", "night")
assertEquals("Great", field.name, "Field name not filled correctly") assertEquals("Great", field.name, "Field name not filled correctly")
@ -30,7 +329,18 @@ class FieldTest {
} }
@Test @Test
@DisplayName("greaterOrEqual constructs a field") @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() { fun greaterOrEqualCtor() {
val field = Field.greaterOrEqual("Nice", 88L) val field = Field.greaterOrEqual("Nice", 88L)
assertEquals("Nice", field.name, "Field name not filled correctly") assertEquals("Nice", field.name, "Field name not filled correctly")
@ -41,7 +351,18 @@ class FieldTest {
} }
@Test @Test
@DisplayName("less constructs a field") @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() { fun lessCtor() {
val field = Field.less("Lesser", "seven") val field = Field.less("Lesser", "seven")
assertEquals("Lesser", field.name, "Field name not filled correctly") assertEquals("Lesser", field.name, "Field name not filled correctly")
@ -52,7 +373,18 @@ class FieldTest {
} }
@Test @Test
@DisplayName("lessOrEqual constructs a field") @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() { fun lessOrEqualCtor() {
val field = Field.lessOrEqual("Nobody", "KNOWS") val field = Field.lessOrEqual("Nobody", "KNOWS")
assertEquals("Nobody", field.name, "Field name not filled correctly") assertEquals("Nobody", field.name, "Field name not filled correctly")
@ -63,7 +395,18 @@ class FieldTest {
} }
@Test @Test
@DisplayName("notEqual constructs a field") @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() { fun notEqualCtor() {
val field = Field.notEqual("Park", "here") val field = Field.notEqual("Park", "here")
assertEquals("Park", field.name, "Field name not filled correctly") assertEquals("Park", field.name, "Field name not filled correctly")
@ -74,7 +417,18 @@ class FieldTest {
} }
@Test @Test
@DisplayName("between constructs a field") @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() { fun betweenCtor() {
val field = Field.between("Age", 18, 49) val field = Field.between("Age", 18, 49)
assertEquals("Age", field.name, "Field name not filled correctly") assertEquals("Age", field.name, "Field name not filled correctly")
@ -86,8 +440,20 @@ class FieldTest {
} }
@Test @Test
@DisplayName("any constructs a field") @DisplayName("between constructs a field w/ parameter name")
fun inCtor() { 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)) val field = Field.any("Here", listOf(8, 16, 32))
assertEquals("Here", field.name, "Field name not filled correctly") assertEquals("Here", field.name, "Field name not filled correctly")
assertEquals(Op.IN, field.comparison.op, "Field comparison operation not filled correctly") assertEquals(Op.IN, field.comparison.op, "Field comparison operation not filled correctly")
@ -97,7 +463,18 @@ class FieldTest {
} }
@Test @Test
@DisplayName("inArray constructs a field") @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() { fun inArrayCtor() {
val field = Field.inArray("ArrayField", "table", listOf("z")) val field = Field.inArray("ArrayField", "table", listOf("z"))
assertEquals("ArrayField", field.name, "Field name not filled correctly") assertEquals("ArrayField", field.name, "Field name not filled correctly")
@ -108,6 +485,18 @@ class FieldTest {
assertNull(field.qualifier, "The qualifier 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 @Test
@DisplayName("exists constructs a field") @DisplayName("exists constructs a field")
fun existsCtor() { fun existsCtor() {
@ -143,135 +532,53 @@ class FieldTest {
@Test @Test
@DisplayName("nameToPath creates a simple PostgreSQL SQL name") @DisplayName("nameToPath creates a simple PostgreSQL SQL name")
fun nameToPathPostgresSimpleSQL() { fun nameToPathPostgresSimpleSQL() =
assertEquals("data->>'Simple'", Field.nameToPath("Simple", Dialect.POSTGRESQL, FieldFormat.SQL), assertEquals("data->>'Simple'", Field.nameToPath("Simple", Dialect.POSTGRESQL, FieldFormat.SQL),
"Path not constructed correctly") "Path not constructed correctly")
}
@Test @Test
@DisplayName("nameToPath creates a simple SQLite SQL name") @DisplayName("nameToPath creates a simple SQLite SQL name")
fun nameToPathSQLiteSimpleSQL() { fun nameToPathSQLiteSimpleSQL() =
assertEquals("data->>'Simple'", Field.nameToPath("Simple", Dialect.SQLITE, FieldFormat.SQL), assertEquals("data->>'Simple'", Field.nameToPath("Simple", Dialect.SQLITE, FieldFormat.SQL),
"Path not constructed correctly") "Path not constructed correctly")
}
@Test @Test
@DisplayName("nameToPath creates a nested PostgreSQL SQL name") @DisplayName("nameToPath creates a nested PostgreSQL SQL name")
fun nameToPathPostgresNestedSQL() { fun nameToPathPostgresNestedSQL() =
assertEquals("data#>>'{A,Long,Path,to,the,Property}'", assertEquals("data#>>'{A,Long,Path,to,the,Property}'",
Field.nameToPath("A.Long.Path.to.the.Property", Dialect.POSTGRESQL, FieldFormat.SQL), Field.nameToPath("A.Long.Path.to.the.Property", Dialect.POSTGRESQL, FieldFormat.SQL),
"Path not constructed correctly") "Path not constructed correctly")
}
@Test @Test
@DisplayName("nameToPath creates a nested SQLite SQL name") @DisplayName("nameToPath creates a nested SQLite SQL name")
fun nameToPathSQLiteNestedSQL() { fun nameToPathSQLiteNestedSQL() =
assertEquals("data->'A'->'Long'->'Path'->'to'->'the'->>'Property'", assertEquals("data->'A'->'Long'->'Path'->'to'->'the'->>'Property'",
Field.nameToPath("A.Long.Path.to.the.Property", Dialect.SQLITE, FieldFormat.SQL), Field.nameToPath("A.Long.Path.to.the.Property", Dialect.SQLITE, FieldFormat.SQL),
"Path not constructed correctly") "Path not constructed correctly")
}
@Test @Test
@DisplayName("nameToPath creates a simple PostgreSQL JSON name") @DisplayName("nameToPath creates a simple PostgreSQL JSON name")
fun nameToPathPostgresSimpleJSON() { fun nameToPathPostgresSimpleJSON() =
assertEquals("data->'Simple'", Field.nameToPath("Simple", Dialect.POSTGRESQL, FieldFormat.JSON), assertEquals("data->'Simple'", Field.nameToPath("Simple", Dialect.POSTGRESQL, FieldFormat.JSON),
"Path not constructed correctly") "Path not constructed correctly")
}
@Test @Test
@DisplayName("nameToPath creates a simple SQLite JSON name") @DisplayName("nameToPath creates a simple SQLite JSON name")
fun nameToPathSQLiteSimpleJSON() { fun nameToPathSQLiteSimpleJSON() =
assertEquals("data->'Simple'", Field.nameToPath("Simple", Dialect.SQLITE, FieldFormat.JSON), assertEquals("data->'Simple'", Field.nameToPath("Simple", Dialect.SQLITE, FieldFormat.JSON),
"Path not constructed correctly") "Path not constructed correctly")
}
@Test @Test
@DisplayName("nameToPath creates a nested PostgreSQL JSON name") @DisplayName("nameToPath creates a nested PostgreSQL JSON name")
fun nameToPathPostgresNestedJSON() { fun nameToPathPostgresNestedJSON() =
assertEquals("data#>'{A,Long,Path,to,the,Property}'", assertEquals("data#>'{A,Long,Path,to,the,Property}'",
Field.nameToPath("A.Long.Path.to.the.Property", Dialect.POSTGRESQL, FieldFormat.JSON), Field.nameToPath("A.Long.Path.to.the.Property", Dialect.POSTGRESQL, FieldFormat.JSON),
"Path not constructed correctly") "Path not constructed correctly")
}
@Test @Test
@DisplayName("nameToPath creates a nested SQLite JSON name") @DisplayName("nameToPath creates a nested SQLite JSON name")
fun nameToPathSQLiteNestedJSON() { fun nameToPathSQLiteNestedJSON() =
assertEquals("data->'A'->'Long'->'Path'->'to'->'the'->'Property'", assertEquals("data->'A'->'Long'->'Path'->'to'->'the'->'Property'",
Field.nameToPath("A.Long.Path.to.the.Property", Dialect.SQLITE, FieldFormat.JSON), Field.nameToPath("A.Long.Path.to.the.Property", Dialect.SQLITE, FieldFormat.JSON),
"Path not constructed correctly") "Path not constructed correctly")
}
@Test
@DisplayName("withParameterName adjusts the parameter name")
fun withParameterName() {
assertEquals(":name", Field.equal("Bob", "Tom").withParameterName(":name").parameterName,
"Parameter name not filled correctly")
}
@Test
@DisplayName("withQualifier adjust the table qualifier")
fun withQualifier() {
assertEquals("joe", Field.equal("Bill", "Matt").withQualifier("joe").qualifier,
"Qualifier not filled 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")
}
} }

View File

@ -1,5 +1,6 @@
package solutions.bitbadger.documents package solutions.bitbadger.documents
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -9,6 +10,14 @@ class QueryTest {
/** Test table name */ /** Test table name */
private val tbl = "test_table" private val tbl = "test_table"
/**
* Clear the connection string (resets Dialect)
*/
@AfterEach
fun cleanUp() {
Configuration.connectionString = null
}
@Test @Test
@DisplayName("statementWhere generates correctly") @DisplayName("statementWhere generates correctly")
fun statementWhere() { fun statementWhere() {
@ -67,30 +76,17 @@ class QueryTest {
@Test @Test
@DisplayName("insert generates correctly") @DisplayName("insert generates correctly")
fun insert() { fun insert() {
try { Configuration.connectionString = ":postgresql:"
Configuration.connectionString = "postgresql" assertEquals("INSERT INTO $tbl VALUES (:data)", Query.insert(tbl), "INSERT statement not constructed correctly")
assertEquals(
"INSERT INTO $tbl VALUES (:data)",
Query.insert(tbl),
"INSERT statement not constructed correctly"
)
} finally {
Configuration.connectionString = null
}
} }
@Test @Test
@DisplayName("save generates correctly") @DisplayName("save generates correctly")
fun save() { fun save() {
try { Configuration.connectionString = ":postgresql:"
Configuration.connectionString = "postgresql"
assertEquals( assertEquals(
"INSERT INTO $tbl VALUES (:data) ON CONFLICT ((data->>'id')) DO UPDATE SET data = EXCLUDED.data", "INSERT INTO $tbl VALUES (:data) ON CONFLICT ((data->>'id')) DO UPDATE SET data = EXCLUDED.data",
Query.save(tbl), "INSERT ON CONFLICT UPDATE statement not constructed correctly" Query.save(tbl), "INSERT ON CONFLICT UPDATE statement not constructed correctly")
)
} finally {
Configuration.connectionString = null
}
} }
@Test @Test