WIP on query tests

This commit is contained in:
Daniel J. Summers 2025-02-18 17:54:18 -05:00
parent 0b11b803cc
commit 2b6dff9fd3
6 changed files with 173 additions and 50 deletions

1
.idea/misc.xml generated
View File

@ -6,6 +6,7 @@
<list>
<option value="$PROJECT_DIR$/src/common/pom.xml" />
<option value="$PROJECT_DIR$/src/sqlite/pom.xml" />
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
<option name="ignoredFiles">

View File

@ -14,7 +14,7 @@ class Comparison<T>(val op: Op, val value: T) {
val toCheck = when (op) {
Op.IN -> {
val values = value as? Collection<*>
if (values.isNullOrEmpty()) "" else values.elementAt(0)
if (values.isNullOrEmpty()) "" else values.elementAt(0)
}
Op.BETWEEN -> (value as Pair<*, *>).first
else -> value

View File

@ -21,7 +21,11 @@ object Parameters {
fun nameFields(fields: Collection<Field<*>>): Collection<Field<*>> {
val name = ParameterName()
return fields.map {
if (it.name.isBlank()) it.withParameterName(name.derive(null)) else it
if (it.parameterName.isNullOrEmpty() && !listOf(Op.EXISTS, Op.NOT_EXISTS).contains(it.comparison.op)) {
it.withParameterName(name.derive(null))
} else {
it
}
}
}
@ -75,7 +79,8 @@ object Parameters {
is Int -> stmt.setInt(idx, param.value)
is Long -> stmt.setLong(idx, param.value)
else -> throw DocumentException(
"Number parameter must be Byte, Short, Int, or Long (${param.value::class.simpleName})")
"Number parameter must be Byte, Short, Int, or Long " +
"(${param.value::class.simpleName})")
}
}
ParameterType.STRING -> {

View File

@ -16,7 +16,7 @@ object Results {
* @param rs A `ResultSet` set to the row with the document to be constructed
* @return The constructed domain item
*/
inline fun <reified TDoc> fromDocument(field: String, rs: ResultSet): TDoc =
inline fun <reified TDoc> fromDocument(field: String, rs: ResultSet) =
Configuration.json.decodeFromString<TDoc>(rs.getString(field))
/**
@ -25,8 +25,8 @@ object Results {
* @param rs A `ResultSet` set to the row with the document to be constructed<
* @return The constructed domain item
*/
inline fun <reified TDoc> fromData(rs: ResultSet): TDoc =
fromDocument("data", rs)
inline fun <reified TDoc> fromData(rs: ResultSet) =
fromDocument<TDoc>("data", rs)
/**
* Create a list of items for the results of the given command, using the specified mapping function
@ -36,7 +36,7 @@ object Results {
* @return A list of items from the query's result
* @throws DocumentException If there is a problem executing the query
*/
inline fun <reified TDoc> toCustomList(stmt: PreparedStatement, mapFunc: (ResultSet) -> TDoc): List<TDoc> =
inline fun <reified TDoc> toCustomList(stmt: PreparedStatement, mapFunc: (ResultSet) -> TDoc) =
try {
stmt.executeQuery().use {
val results = mutableListOf<TDoc>()
@ -55,7 +55,7 @@ object Results {
* @param rs A `ResultSet` set to the row with the count to retrieve
* @return The count from the row
*/
fun toCount(rs: ResultSet): Long =
fun toCount(rs: ResultSet) =
when (Configuration.dialect()) {
Dialect.POSTGRESQL -> rs.getInt("it").toLong()
Dialect.SQLITE -> rs.getLong("it")
@ -67,7 +67,7 @@ object Results {
* @param rs A `ResultSet` set to the row with the true/false value to retrieve
* @return The true/false value from the row
*/
fun toExists(rs: ResultSet): Boolean =
fun toExists(rs: ResultSet) =
when (Configuration.dialect()) {
Dialect.POSTGRESQL -> rs.getBoolean("it")
Dialect.SQLITE -> toCount(rs) > 0L

View File

@ -3,9 +3,37 @@ package solutions.bitbadger.documents
import org.junit.jupiter.api.DisplayName
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotSame
import kotlin.test.assertSame
class ParametersTest {
@Test
@DisplayName("nameFields works with no changes")
fun nameFieldsNoChange() {
val fields = listOf(Field.equal("a", "", ":test"), Field.exists("q"), Field.equal("b", "", ":me"))
val named = Parameters.nameFields(fields)
assertEquals(fields.size, named.size, "There should have been 3 fields in the list")
assertSame(fields.elementAt(0), named.elementAt(0), "The first field should be the same")
assertSame(fields.elementAt(1), named.elementAt(1), "The second field should be the same")
assertSame(fields.elementAt(2), named.elementAt(2), "The third field should be the same")
}
@Test
@DisplayName("nameFields works when changing fields")
fun nameFieldsChange() {
val fields = listOf(
Field.equal("a", ""), Field.equal("e", "", ":hi"), Field.equal("b", ""), Field.notExists("z"))
val named = Parameters.nameFields(fields)
assertEquals(fields.size, named.size, "There should have been 4 fields in the list")
assertNotSame(fields.elementAt(0), named.elementAt(0), "The first field should not be the same")
assertEquals(":field0", named.elementAt(0).parameterName, "First parameter name incorrect")
assertSame(fields.elementAt(1), named.elementAt(1), "The second field should be the same")
assertNotSame(fields.elementAt(2), named.elementAt(2), "The third field should not be the same")
assertEquals(":field1", named.elementAt(2).parameterName, "Third parameter name incorrect")
assertSame(fields.elementAt(3), named.elementAt(3), "The fourth field should be the same")
}
@Test
@DisplayName("replaceNamesInQuery replaces successfully")
fun replaceNamesInQuery() {

View File

@ -10,6 +10,12 @@ class QueryTest {
/** Test table name */
private val tbl = "test_table"
/** Dummy connection string for PostgreSQL */
private val pg = ":postgresql:"
/** Dummy connection string for SQLite */
private val lite = ":sqlite:"
/**
* Clear the connection string (resets Dialect)
*/
@ -20,70 +26,166 @@ class QueryTest {
@Test
@DisplayName("statementWhere generates correctly")
fun statementWhere() {
fun statementWhere() =
assertEquals("x WHERE y", Query.statementWhere("x", "y"), "Statements not combined correctly")
// ~~~ Where ~~~
@Test
@DisplayName("Where.byFields is blank when given no fields")
fun whereByFieldsBlankIfEmpty() =
assertEquals("", Query.Where.byFields(listOf()))
@Test
@DisplayName("Where.byFields generates one numeric field (PostgreSQL)")
fun whereByFieldsOneFieldPostgres() {
Configuration.connectionString = pg
assertEquals("(data->>'it')::numeric = :that", Query.Where.byFields(listOf(Field.equal("it", 9, ":that"))))
}
@Test
@DisplayName("Where.byFields generates one alphanumeric field (PostgreSQL)")
fun whereByFieldsOneAlphaFieldPostgres() {
Configuration.connectionString = pg
assertEquals("data->>'it' = :that", Query.Where.byFields(listOf(Field.equal("it", "", ":that"))))
}
@Test
@DisplayName("Where.byFields generates one field (SQLite)")
fun whereByFieldsOneFieldSQLite() {
Configuration.connectionString = lite
assertEquals("data->>'it' = :that", Query.Where.byFields(listOf(Field.equal("it", "", ":that"))))
}
@Test
@DisplayName("Where.byFields generates multiple fields w/ default match (PostgreSQL)")
fun whereByFieldsMultipleDefaultPostgres() {
Configuration.connectionString = pg
assertEquals("data->>'1' = :one AND (data->>'2')::numeric = :two AND data->>'3' = :three",
Query.Where.byFields(
listOf(Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three"))))
}
@Test
@DisplayName("Where.byFields generates multiple fields w/ default match (SQLite)")
fun whereByFieldsMultipleDefaultSQLite() {
Configuration.connectionString = lite
assertEquals("data->>'1' = :one AND data->>'2' = :two AND data->>'3' = :three",
Query.Where.byFields(
listOf(Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three"))))
}
@Test
@DisplayName("Where.byFields generates multiple fields w/ ANY match (PostgreSQL)")
fun whereByFieldsMultipleAnyPostgres() {
Configuration.connectionString = pg
assertEquals("data->>'1' = :one OR (data->>'2')::numeric = :two OR data->>'3' = :three",
Query.Where.byFields(
listOf(Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three")),
FieldMatch.ANY))
}
@Test
@DisplayName("Where.byFields generates multiple fields w/ ANY match (SQLite)")
fun whereByFieldsMultipleAnySQLite() {
Configuration.connectionString = lite
assertEquals("data->>'1' = :one OR data->>'2' = :two OR data->>'3' = :three",
Query.Where.byFields(
listOf(Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three")),
FieldMatch.ANY))
}
@Test
@DisplayName("Where.byId generates defaults for alphanumeric key (PostgreSQL)")
fun whereByIdDefaultAlphaPostgres() {
Configuration.connectionString = pg
assertEquals("data->>'id' = :id", Query.Where.byId(docId = ""))
}
@Test
@DisplayName("Where.byId generates defaults for numeric key (PostgreSQL)")
fun whereByIdDefaultNumericPostgres() {
Configuration.connectionString = pg
assertEquals("(data->>'id')::numeric = :id", Query.Where.byId(docId = 5))
}
@Test
@DisplayName("Where.byId generates defaults (SQLite)")
fun whereByIdDefaultSQLite() {
Configuration.connectionString = lite
assertEquals("data->>'id' = :id", Query.Where.byId(docId = ""))
}
@Test
@DisplayName("Where.byId generates named ID (PostgreSQL)")
fun whereByIdDefaultNamedPostgres() {
Configuration.connectionString = pg
assertEquals("data->>'id' = :key", Query.Where.byId<String>(":key"))
}
@Test
@DisplayName("Where.byId generates named ID (SQLite)")
fun whereByIdDefaultNamedSQLite() {
Configuration.connectionString = lite
assertEquals("data->>'id' = :key", Query.Where.byId<String>(":key"))
}
// ~~~ Definition ~~~
@Test
@DisplayName("Definition.ensureTableFor generates correctly")
fun ensureTableFor() {
fun ensureTableFor() =
assertEquals("CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)",
Query.Definition.ensureTableFor("my.table", "JSONB"), "CREATE TABLE statement not constructed correctly")
}
@Test
@DisplayName("Definition.ensureKey generates correctly with schema")
fun ensureKeyWithSchema() {
fun ensureKeyWithSchema() =
assertEquals("CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'id'))",
Query.Definition.ensureKey("test.table", Dialect.POSTGRESQL),
"CREATE INDEX for key statement with schema not constructed correctly")
}
@Test
@DisplayName("Definition.ensureKey generates correctly without schema")
fun ensureKeyWithoutSchema() {
fun ensureKeyWithoutSchema() =
assertEquals("CREATE UNIQUE INDEX IF NOT EXISTS idx_${tbl}_key ON $tbl ((data->>'id'))",
Query.Definition.ensureKey(tbl, Dialect.SQLITE),
"CREATE INDEX for key statement without schema not constructed correctly")
}
@Test
@DisplayName("Definition.ensureIndexOn generates multiple fields and directions")
fun ensureIndexOnMultipleFields() {
fun ensureIndexOnMultipleFields() =
assertEquals(
"CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table ((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)",
Query.Definition.ensureIndexOn("test.table", "gibberish", listOf("taco", "guac DESC", "salsa ASC"),
Dialect.POSTGRESQL),
"CREATE INDEX for multiple field statement not constructed correctly")
}
@Test
@DisplayName("Definition.ensureIndexOn generates nested PostgreSQL field")
fun ensureIndexOnNestedPostgres() {
fun ensureIndexOnNestedPostgres() =
assertEquals("CREATE INDEX IF NOT EXISTS idx_${tbl}_nest ON $tbl ((data#>>'{a,b,c}'))",
Query.Definition.ensureIndexOn(tbl, "nest", listOf("a.b.c"), Dialect.POSTGRESQL),
"CREATE INDEX for nested PostgreSQL field incorrect")
}
@Test
@DisplayName("Definition.ensureIndexOn generates nested SQLite field")
fun ensureIndexOnNestedSQLite() {
fun ensureIndexOnNestedSQLite() =
assertEquals("CREATE INDEX IF NOT EXISTS idx_${tbl}_nest ON $tbl ((data->'a'->'b'->>'c'))",
Query.Definition.ensureIndexOn(tbl, "nest", listOf("a.b.c"), Dialect.SQLITE),
"CREATE INDEX for nested SQLite field incorrect")
}
@Test
@DisplayName("insert generates correctly")
fun insert() {
Configuration.connectionString = ":postgresql:"
Configuration.connectionString = pg
assertEquals("INSERT INTO $tbl VALUES (:data)", Query.insert(tbl), "INSERT statement not constructed correctly")
}
@Test
@DisplayName("save generates correctly")
fun save() {
Configuration.connectionString = ":postgresql:"
Configuration.connectionString = pg
assertEquals(
"INSERT INTO $tbl VALUES (:data) ON CONFLICT ((data->>'id')) DO UPDATE SET data = EXCLUDED.data",
Query.save(tbl), "INSERT ON CONFLICT UPDATE statement not constructed correctly")
@ -91,34 +193,29 @@ class QueryTest {
@Test
@DisplayName("count generates correctly")
fun count() {
fun count() =
assertEquals("SELECT COUNT(*) AS it FROM $tbl", Query.count(tbl), "Count query not constructed correctly")
}
@Test
@DisplayName("exists generates correctly")
fun exists() {
fun exists() =
assertEquals("SELECT EXISTS (SELECT 1 FROM $tbl WHERE turkey) AS it", Query.exists(tbl, "turkey"),
"Exists query not constructed correctly")
}
@Test
@DisplayName("find generates correctly")
fun find() {
fun find() =
assertEquals("SELECT data FROM $tbl", Query.find(tbl), "Find query not constructed correctly")
}
@Test
@DisplayName("update generates successfully")
fun update() {
fun update() =
assertEquals("UPDATE $tbl SET data = :data", Query.update(tbl), "Update query not constructed correctly")
}
@Test
@DisplayName("delete generates successfully")
fun delete() {
fun delete() =
assertEquals("DELETE FROM $tbl", Query.delete(tbl), "Delete query not constructed correctly")
}
@Test
@DisplayName("orderBy generates for no fields")
@ -129,65 +226,57 @@ class QueryTest {
@Test
@DisplayName("orderBy generates single, no direction for PostgreSQL")
fun orderBySinglePostgres() {
fun orderBySinglePostgres() =
assertEquals(" ORDER BY data->>'TestField'",
Query.orderBy(listOf(Field.named("TestField")), Dialect.POSTGRESQL), "ORDER BY not constructed correctly")
}
@Test
@DisplayName("orderBy generates single, no direction for SQLite")
fun orderBySingleSQLite() {
fun orderBySingleSQLite() =
assertEquals(" ORDER BY data->>'TestField'", Query.orderBy(listOf(Field.named("TestField")), Dialect.SQLITE),
"ORDER BY not constructed correctly")
}
@Test
@DisplayName("orderBy generates multiple with direction for PostgreSQL")
fun orderByMultiplePostgres() {
fun orderByMultiplePostgres() =
assertEquals(" ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC",
Query.orderBy(
listOf(Field.named("Nested.Test.Field DESC"), Field.named("AnotherField"), Field.named("It DESC")),
Dialect.POSTGRESQL),
"ORDER BY not constructed correctly")
}
@Test
@DisplayName("orderBy generates multiple with direction for SQLite")
fun orderByMultipleSQLite() {
fun orderByMultipleSQLite() =
assertEquals(" ORDER BY data->'Nested'->'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC",
Query.orderBy(
listOf(Field.named("Nested.Test.Field DESC"), Field.named("AnotherField"), Field.named("It DESC")),
Dialect.SQLITE),
"ORDER BY not constructed correctly")
}
@Test
@DisplayName("orderBy generates numeric ordering PostgreSQL")
fun orderByNumericPostgres() {
fun orderByNumericPostgres() =
assertEquals(" ORDER BY (data->>'Test')::numeric",
Query.orderBy(listOf(Field.named("n:Test")), Dialect.POSTGRESQL), "ORDER BY not constructed correctly")
}
@Test
@DisplayName("orderBy generates numeric ordering for SQLite")
fun orderByNumericSQLite() {
fun orderByNumericSQLite() =
assertEquals(" ORDER BY data->>'Test'", Query.orderBy(listOf(Field.named("n:Test")), Dialect.SQLITE),
"ORDER BY not constructed correctly")
}
@Test
@DisplayName("orderBy generates case-insensitive ordering for PostgreSQL")
fun orderByCIPostgres() {
fun orderByCIPostgres() =
assertEquals(" ORDER BY LOWER(data#>>'{Test,Field}') DESC NULLS FIRST",
Query.orderBy(listOf(Field.named("i:Test.Field DESC NULLS FIRST")), Dialect.POSTGRESQL),
"ORDER BY not constructed correctly")
}
@Test
@DisplayName("orderBy generates case-insensitive ordering for SQLite")
fun orderByCISQLite() {
fun orderByCISQLite() =
assertEquals(" ORDER BY data->'Test'->>'Field' COLLATE NOCASE ASC NULLS LAST",
Query.orderBy(listOf(Field.named("i:Test.Field ASC NULLS LAST")), Dialect.SQLITE),
"ORDER BY not constructed correctly")
}
}