diff --git a/.idea/misc.xml b/.idea/misc.xml index 9959fd1..e56fe0c 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -6,6 +6,7 @@ + diff --git a/src/main/kotlin/Comparison.kt b/src/main/kotlin/Comparison.kt index 987f946..c4bdd05 100644 --- a/src/main/kotlin/Comparison.kt +++ b/src/main/kotlin/Comparison.kt @@ -14,7 +14,7 @@ class Comparison(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 diff --git a/src/main/kotlin/Parameters.kt b/src/main/kotlin/Parameters.kt index 550ecf0..cf54774 100644 --- a/src/main/kotlin/Parameters.kt +++ b/src/main/kotlin/Parameters.kt @@ -21,7 +21,11 @@ object Parameters { fun nameFields(fields: Collection>): Collection> { 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 -> { diff --git a/src/main/kotlin/Results.kt b/src/main/kotlin/Results.kt index f35b483..1ab07fc 100644 --- a/src/main/kotlin/Results.kt +++ b/src/main/kotlin/Results.kt @@ -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 fromDocument(field: String, rs: ResultSet): TDoc = + inline fun fromDocument(field: String, rs: ResultSet) = Configuration.json.decodeFromString(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 fromData(rs: ResultSet): TDoc = - fromDocument("data", rs) + inline fun fromData(rs: ResultSet) = + fromDocument("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 toCustomList(stmt: PreparedStatement, mapFunc: (ResultSet) -> TDoc): List = + inline fun toCustomList(stmt: PreparedStatement, mapFunc: (ResultSet) -> TDoc) = try { stmt.executeQuery().use { val results = mutableListOf() @@ -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 diff --git a/src/test/kotlin/ParametersTest.kt b/src/test/kotlin/ParametersTest.kt index d5a6592..1901df7 100644 --- a/src/test/kotlin/ParametersTest.kt +++ b/src/test/kotlin/ParametersTest.kt @@ -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() { diff --git a/src/test/kotlin/QueryTest.kt b/src/test/kotlin/QueryTest.kt index 16de55a..a437dd3 100644 --- a/src/test/kotlin/QueryTest.kt +++ b/src/test/kotlin/QueryTest.kt @@ -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(":key")) + } + + @Test + @DisplayName("Where.byId generates named ID (SQLite)") + fun whereByIdDefaultNamedSQLite() { + Configuration.connectionString = lite + assertEquals("data->>'id' = :key", Query.Where.byId(":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") - } }