From cefd2daa520790426f46da99197f963c9486d2b0 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Tue, 18 Feb 2025 22:57:06 -0500 Subject: [PATCH] WIP on query tests --- src/main/kotlin/Query.kt | 18 ++- src/test/kotlin/QueryTest.kt | 213 ++++++++++++++++++++++++++++++++++- 2 files changed, 218 insertions(+), 13 deletions(-) diff --git a/src/main/kotlin/Query.kt b/src/main/kotlin/Query.kt index 6852f91..bb4774f 100644 --- a/src/main/kotlin/Query.kt +++ b/src/main/kotlin/Query.kt @@ -46,7 +46,7 @@ object Query { 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") + Dialect.SQLITE -> throw DocumentException("JSON containment is not supported") } /** @@ -59,7 +59,7 @@ object Query { 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") + Dialect.SQLITE -> throw DocumentException("JSON path match is not supported") } } @@ -77,12 +77,12 @@ object Query { * Create a query on JSON fields * * @param statement The SQL statement to be run against matching fields - * @param howMatched Whether to match any or all of the field conditions * @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, howMatched: FieldMatch, fields: Collection>) = - Query.statementWhere(statement, Where.byFields(fields, howMatched)) + fun byFields(statement: String, fields: Collection>, howMatched: FieldMatch? = null) = + statementWhere(statement, Where.byFields(fields, howMatched)) /** * Functions to create queries to define tables and indexes @@ -108,7 +108,7 @@ object Query { fun ensureTable(tableName: String) = when (Configuration.dialect("create table creation query")) { Dialect.POSTGRESQL -> ensureTableFor(tableName, "JSONB") - Dialect.SQLITE -> ensureTableFor(tableName, "TEXT") + Dialect.SQLITE -> ensureTableFor(tableName, "TEXT") } /** @@ -117,10 +117,8 @@ object Query { * @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): Pair { - val parts = tableName.split('.') - return if (parts.size == 1) Pair("", tableName) else Pair(parts[0], parts[1]) - } + 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 diff --git a/src/test/kotlin/QueryTest.kt b/src/test/kotlin/QueryTest.kt index a437dd3..d8f6233 100644 --- a/src/test/kotlin/QueryTest.kt +++ b/src/test/kotlin/QueryTest.kt @@ -3,7 +3,9 @@ package solutions.bitbadger.documents import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows import kotlin.test.assertEquals +import kotlin.test.assertTrue class QueryTest { @@ -130,6 +132,105 @@ class QueryTest { assertEquals("data->>'id' = :key", Query.Where.byId(":key")) } + @Test + @DisplayName("Where.jsonContains generates defaults (PostgreSQL)") + fun whereJsonContainsDefaultPostgres() { + Configuration.connectionString = pg + assertEquals("data @> :criteria", Query.Where.jsonContains()) + } + + @Test + @DisplayName("Where.jsonContains generates named parameter (PostgreSQL)") + fun whereJsonContainsNamedPostgres() { + Configuration.connectionString = pg + assertEquals("data @> :it", Query.Where.jsonContains(":it")) + } + + @Test + @DisplayName("Where.jsonContains fails (SQLite)") + fun whereJsonContainsFailsSQLite() { + Configuration.connectionString = lite + assertThrows { Query.Where.jsonContains() } + } + + @Test + @DisplayName("Where.jsonPathMatch generates defaults (PostgreSQL)") + fun whereJsonPathMatchDefaultPostgres() { + Configuration.connectionString = pg + assertEquals("jsonb_path_exists(data, :path::jsonpath)", Query.Where.jsonPathMatches()) + } + + @Test + @DisplayName("Where.jsonPathMatch generates named parameter (PostgreSQL)") + fun whereJsonPathMatchNamedPostgres() { + Configuration.connectionString = pg + assertEquals("jsonb_path_exists(data, :jp::jsonpath)", Query.Where.jsonPathMatches(":jp")) + } + + @Test + @DisplayName("Where.jsonPathMatch fails (SQLite)") + fun whereJsonPathFailsSQLite() { + Configuration.connectionString = lite + assertThrows { Query.Where.jsonPathMatches() } + } + + // ~~~ root functions ~~~ + + @Test + @DisplayName("byId generates a numeric ID query (PostgreSQL)") + fun byIdNumericPostgres() { + Configuration.connectionString = pg + assertEquals("test WHERE (data->>'id')::numeric = :id", Query.byId("test", 9)) + } + + @Test + @DisplayName("byId generates an alphanumeric ID query (PostgreSQL)") + fun byIdAlphaPostgres() { + Configuration.connectionString = pg + assertEquals("unit WHERE data->>'id' = :id", Query.byId("unit", "18")) + } + + @Test + @DisplayName("byId generates ID query (SQLite)") + fun byIdSQLite() { + Configuration.connectionString = lite + assertEquals("yo WHERE data->>'id' = :id", Query.byId("yo", 27)) + } + + @Test + @DisplayName("byFields generates default field query (PostgreSQL)") + fun byFieldsMultipleDefaultPostgres() { + Configuration.connectionString = pg + assertEquals("this WHERE data->>'a' = :the_a AND (data->>'b')::numeric = :b_value", + Query.byFields("this", listOf(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")))) + } + + @Test + @DisplayName("byFields generates default field query (SQLite)") + fun byFieldsMultipleDefaultSQLite() { + Configuration.connectionString = lite + assertEquals("this WHERE data->>'a' = :the_a AND data->>'b' = :b_value", + Query.byFields("this", listOf(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")))) + } + + @Test + @DisplayName("byFields generates ANY field query (PostgreSQL)") + fun byFieldsMultipleAnyPostgres() { + Configuration.connectionString = pg + assertEquals("that WHERE data->>'a' = :the_a OR (data->>'b')::numeric = :b_value", + Query.byFields("that", listOf(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")), + FieldMatch.ANY)) + } + + @Test + @DisplayName("byFields generates ANY field query (SQLite)") + fun byFieldsMultipleAnySQLite() { + Configuration.connectionString = lite + assertEquals("that WHERE data->>'a' = :the_a OR data->>'b' = :b_value", + Query.byFields("that", listOf(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")), + FieldMatch.ANY)) + } + // ~~~ Definition ~~~ @Test @@ -138,6 +239,26 @@ class QueryTest { assertEquals("CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)", Query.Definition.ensureTableFor("my.table", "JSONB"), "CREATE TABLE statement not constructed correctly") + @Test + @DisplayName("Definition.ensureTable generates correctly (PostgreSQL)") + fun ensureTablePostgres() { + Configuration.connectionString = pg + assertEquals("CREATE TABLE IF NOT EXISTS $tbl (data JSONB NOT NULL)", Query.Definition.ensureTable(tbl)) + } + + @Test + @DisplayName("Definition.ensureTable generates correctly (SQLite)") + fun ensureTableSQLite() { + Configuration.connectionString = lite + assertEquals("CREATE TABLE IF NOT EXISTS $tbl (data TEXT NOT NULL)", Query.Definition.ensureTable(tbl)) + } + + @Test + @DisplayName("Definition.ensureTable fails when no dialect is set") + fun ensureTableFailsUnknown() { + assertThrows { Query.Definition.ensureTable(tbl) } + } + @Test @DisplayName("Definition.ensureKey generates correctly with schema") fun ensureKeyWithSchema() = @@ -175,11 +296,97 @@ class QueryTest { Query.Definition.ensureIndexOn(tbl, "nest", listOf("a.b.c"), Dialect.SQLITE), "CREATE INDEX for nested SQLite field incorrect") + // ~~~ root functions ~~~ + @Test - @DisplayName("insert generates correctly") - fun insert() { + @DisplayName("insert generates no auto ID (PostgreSQL)") + fun insertNoAutoPostgres() { Configuration.connectionString = pg - assertEquals("INSERT INTO $tbl VALUES (:data)", Query.insert(tbl), "INSERT statement not constructed correctly") + assertEquals("INSERT INTO $tbl VALUES (:data)", Query.insert(tbl)) + } + + @Test + @DisplayName("insert generates no auto ID (SQLite)") + fun insertNoAutoSQLite() { + Configuration.connectionString = lite + assertEquals("INSERT INTO $tbl VALUES (:data)", Query.insert(tbl)) + } + + @Test + @DisplayName("insert generates auto number (PostgreSQL)") + fun insertAutoNumberPostgres() { + Configuration.connectionString = pg + assertEquals( + "INSERT INTO $tbl VALUES (:data::jsonb || ('{\"id\":' " + + "|| (SELECT COALESCE(MAX((data->>'id')::numeric), 0) + 1 FROM $tbl) || '}')::jsonb)", + Query.insert(tbl, AutoId.NUMBER)) + } + + @Test + @DisplayName("insert generates auto number (SQLite)") + fun insertAutoNumberSQLite() { + Configuration.connectionString = lite + assertEquals( + "INSERT INTO $tbl VALUES (json_set(:data, '$.id', " + + "(SELECT coalesce(max(data->>'id'), 0) + 1 FROM $tbl)))", + Query.insert(tbl, AutoId.NUMBER)) + } + + @Test + @DisplayName("insert generates auto UUID (PostgreSQL)") + fun insertAutoUUIDPostgres() { + Configuration.connectionString = pg + val query = Query.insert(tbl, AutoId.UUID) + assertTrue(query.startsWith("INSERT INTO $tbl VALUES (:data::jsonb || '{\"id\":\""), + "Query start not correct (actual: $query)") + assertTrue(query.endsWith("\"}')"), "Query end not correct") + } + + @Test + @DisplayName("insert generates auto UUID (SQLite)") + fun insertAutoUUIDSQLite() { + Configuration.connectionString = lite + val query = Query.insert(tbl, AutoId.UUID) + assertTrue(query.startsWith("INSERT INTO $tbl VALUES (json_set(:data, '$.id', '"), + "Query start not correct (actual: $query)") + assertTrue(query.endsWith("'))"), "Query end not correct") + } + + @Test + @DisplayName("insert generates auto random string (PostgreSQL)") + fun insertAutoRandomPostgres() { + try { + Configuration.connectionString = pg + Configuration.idStringLength = 8 + val query = Query.insert(tbl, AutoId.RANDOM_STRING) + assertTrue(query.startsWith("INSERT INTO $tbl VALUES (:data::jsonb || '{\"id\":\""), + "Query start not correct (actual: $query)") + assertTrue(query.endsWith("\"}')"), "Query end not correct") + assertEquals(8, + query.replace("INSERT INTO $tbl VALUES (:data::jsonb || '{\"id\":\"", "").replace("\"}')", "").length, + "Random string length incorrect") + } finally { + Configuration.idStringLength = 16 + } + } + + @Test + @DisplayName("insert generates auto random string (SQLite)") + fun insertAutoRandomSQLite() { + Configuration.connectionString = lite + val query = Query.insert(tbl, AutoId.RANDOM_STRING) + assertTrue(query.startsWith("INSERT INTO $tbl VALUES (json_set(:data, '$.id', '"), + "Query start not correct (actual: $query)") + assertTrue(query.endsWith("'))"), "Query end not correct") + assertEquals(Configuration.idStringLength, + query.replace("INSERT INTO $tbl VALUES (json_set(:data, '$.id', '", "").replace("'))", "").length, + "Random string length incorrect") + } + + @Test + @DisplayName("insert fails when no dialect is set") + fun insertFailsUnknown() { + assertThrows { Query.insert(tbl) } } @Test