From 31817d5cec7ac154e24bdc12f032ece75872888e Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 22 Feb 2025 08:43:14 -0500 Subject: [PATCH] Add exists tests --- src/main/kotlin/Query.kt | 4 +- src/test/kotlin/QueryTest.kt | 304 ++++++++++++++++++++++++++--------- 2 files changed, 228 insertions(+), 80 deletions(-) diff --git a/src/main/kotlin/Query.kt b/src/main/kotlin/Query.kt index bd58dcf..d4da1f7 100644 --- a/src/main/kotlin/Query.kt +++ b/src/main/kotlin/Query.kt @@ -254,7 +254,7 @@ object Query { * @param docId The ID of the document (optional, used for type checking) * @return A query to determine document existence by ID */ - fun byId(tableName: String, docId: TKey?) = + fun byId(tableName: String, docId: TKey? = null) = exists(tableName, Where.byId(docId = docId)) /** @@ -265,7 +265,7 @@ object Query { * @param howMatched How fields should be compared (optional, defaults to ALL) * @return A query to determine document existence for the given fields */ - fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch?) = + fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = exists(tableName, Where.byFields(fields, howMatched)) /** diff --git a/src/test/kotlin/QueryTest.kt b/src/test/kotlin/QueryTest.kt index d257501..ec8c19e 100644 --- a/src/test/kotlin/QueryTest.kt +++ b/src/test/kotlin/QueryTest.kt @@ -63,38 +63,50 @@ class QueryTest { @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", + 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")))) + 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", + 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")))) + 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", + 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)) + 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", + 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)) + FieldMatch.ANY + ) + ) } @Test @@ -201,34 +213,46 @@ class QueryTest { @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")))) + 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")))) + 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)) + 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)) + 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 ~~~ @@ -236,8 +260,10 @@ class QueryTest { @Test @DisplayName("Definition.ensureTableFor generates correctly") 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") + 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)") @@ -262,39 +288,50 @@ class QueryTest { @Test @DisplayName("Definition.ensureKey generates correctly with schema") fun ensureKeyWithSchema() = - assertEquals("CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'id'))", + 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") + "CREATE INDEX for key statement with schema not constructed correctly" + ) @Test @DisplayName("Definition.ensureKey generates correctly without schema") fun ensureKeyWithoutSchema() = - assertEquals("CREATE UNIQUE INDEX IF NOT EXISTS idx_${tbl}_key ON $tbl ((data->>'id'))", + 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") + "CREATE INDEX for key statement without schema not constructed correctly" + ) @Test @DisplayName("Definition.ensureIndexOn generates multiple fields and directions") fun ensureIndexOnMultipleFields() = assertEquals( "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table ((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)", - Query.Definition.ensureIndexOn("test.table", "gibberish", listOf("taco", "guac DESC", "salsa ASC"), - Dialect.POSTGRESQL), - "CREATE INDEX for multiple field statement not constructed correctly") + 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() = - assertEquals("CREATE INDEX IF NOT EXISTS idx_${tbl}_nest ON $tbl ((data#>>'{a,b,c}'))", + 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") + "CREATE INDEX for nested PostgreSQL field incorrect" + ) @Test @DisplayName("Definition.ensureIndexOn generates nested SQLite field") fun ensureIndexOnNestedSQLite() = - assertEquals("CREATE INDEX IF NOT EXISTS idx_${tbl}_nest ON $tbl ((data->'a'->'b'->>'c'))", + 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") + "CREATE INDEX for nested SQLite field incorrect" + ) // ~~~ root functions ~~~ @@ -319,7 +356,8 @@ class QueryTest { assertEquals( "INSERT INTO $tbl VALUES (:data::jsonb || ('{\"id\":' " + "|| (SELECT COALESCE(MAX((data->>'id')::numeric), 0) + 1 FROM $tbl) || '}')::jsonb)", - Query.insert(tbl, AutoId.NUMBER)) + Query.insert(tbl, AutoId.NUMBER) + ) } @Test @@ -329,7 +367,8 @@ class QueryTest { assertEquals( "INSERT INTO $tbl VALUES (json_set(:data, '$.id', " + "(SELECT coalesce(max(data->>'id'), 0) + 1 FROM $tbl)))", - Query.insert(tbl, AutoId.NUMBER)) + Query.insert(tbl, AutoId.NUMBER) + ) } @Test @@ -337,8 +376,10 @@ class QueryTest { 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.startsWith("INSERT INTO $tbl VALUES (:data::jsonb || '{\"id\":\""), + "Query start not correct (actual: $query)" + ) assertTrue(query.endsWith("\"}')"), "Query end not correct") } @@ -347,8 +388,10 @@ class QueryTest { 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.startsWith("INSERT INTO $tbl VALUES (json_set(:data, '$.id', '"), + "Query start not correct (actual: $query)" + ) assertTrue(query.endsWith("'))"), "Query end not correct") } @@ -357,14 +400,18 @@ class QueryTest { fun insertAutoRandomPostgres() { try { Configuration.connectionString = pg - Configuration.idStringLength = 8 + 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.startsWith("INSERT INTO $tbl VALUES (:data::jsonb || '{\"id\":\""), + "Query start not correct (actual: $query)" + ) assertTrue(query.endsWith("\"}')"), "Query end not correct") - assertEquals(8, + assertEquals( + 8, query.replace("INSERT INTO $tbl VALUES (:data::jsonb || '{\"id\":\"", "").replace("\"}')", "").length, - "Random string length incorrect") + "Random string length incorrect" + ) } finally { Configuration.idStringLength = 16 } @@ -375,12 +422,16 @@ class QueryTest { 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.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, + assertEquals( + Configuration.idStringLength, query.replace("INSERT INTO $tbl VALUES (json_set(:data, '$.id', '", "").replace("'))", "").length, - "Random string length incorrect") + "Random string length incorrect" + ) } @Test @@ -395,7 +446,8 @@ class QueryTest { 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") + Query.save(tbl), "INSERT ON CONFLICT UPDATE statement not constructed correctly" + ) } @Test @@ -407,26 +459,32 @@ class QueryTest { @DisplayName("Count.byFields generates correctly (PostgreSQL)") fun countByFieldsPostgres() { Configuration.connectionString = pg - assertEquals("SELECT COUNT(*) AS it FROM $tbl WHERE data->>'test' = :field0", + assertEquals( + "SELECT COUNT(*) AS it FROM $tbl WHERE data->>'test' = :field0", Query.Count.byFields(tbl, listOf(Field.equal("test", "", ":field0"))), - "Count query not constructed correctly") + "Count query not constructed correctly" + ) } @Test @DisplayName("Count.byFields generates correctly (PostgreSQL)") fun countByFieldsSQLite() { Configuration.connectionString = lite - assertEquals("SELECT COUNT(*) AS it FROM $tbl WHERE data->>'test' = :field0", + assertEquals( + "SELECT COUNT(*) AS it FROM $tbl WHERE data->>'test' = :field0", Query.Count.byFields(tbl, listOf(Field.equal("test", "", ":field0"))), - "Count query not constructed correctly") + "Count query not constructed correctly" + ) } @Test @DisplayName("Count.byContains generates correctly (PostgreSQL)") fun countByContainsPostgres() { Configuration.connectionString = pg - assertEquals("SELECT COUNT(*) AS it FROM $tbl WHERE data @> :criteria", Query.Count.byContains(tbl), - "Count query not constructed correctly") + assertEquals( + "SELECT COUNT(*) AS it FROM $tbl WHERE data @> :criteria", Query.Count.byContains(tbl), + "Count query not constructed correctly" + ) } @Test @@ -440,8 +498,10 @@ class QueryTest { @DisplayName("Count.byJsonPath generates correctly (PostgreSQL)") fun countByJsonPathPostgres() { Configuration.connectionString = pg - assertEquals("SELECT COUNT(*) AS it FROM $tbl WHERE jsonb_path_exists(data, :path::jsonpath)", - Query.Count.byJsonPath(tbl), "Count query not constructed correctly") + assertEquals( + "SELECT COUNT(*) AS it FROM $tbl WHERE jsonb_path_exists(data, :path::jsonpath)", + Query.Count.byJsonPath(tbl), "Count query not constructed correctly" + ) } @Test @@ -451,11 +511,81 @@ class QueryTest { assertThrows { Query.Count.byJsonPath(tbl) } } -// @Test -// @DisplayName("exists generates correctly") -// fun exists() = -// assertEquals("SELECT EXISTS (SELECT 1 FROM $tbl WHERE turkey) AS it", Query.exists(tbl, "turkey"), -// "Exists query not constructed correctly") + @Test + @DisplayName("Exists.byId generates correctly (PostgreSQL)") + fun existsByIdPostgres() { + Configuration.connectionString = pg + assertEquals( + "SELECT EXISTS (SELECT 1 FROM $tbl WHERE data->>'id' = :id) AS it", + Query.Exists.byId(tbl), "Exists query not constructed correctly" + ) + } + + @Test + @DisplayName("Exists.byId generates correctly (SQLite)") + fun existsByIdSQLite() { + Configuration.connectionString = lite + assertEquals( + "SELECT EXISTS (SELECT 1 FROM $tbl WHERE data->>'id' = :id) AS it", + Query.Exists.byId(tbl), "Exists query not constructed correctly" + ) + } + + @Test + @DisplayName("Exists.byFields generates correctly (PostgreSQL)") + fun existsByFieldsPostgres() { + Configuration.connectionString = pg + assertEquals( + "SELECT EXISTS (SELECT 1 FROM $tbl WHERE (data->>'it')::numeric = :test) AS it", + Query.Exists.byFields(tbl, listOf(Field.equal("it", 7, ":test"))), + "Exists query not constructed correctly" + ) + } + + @Test + @DisplayName("Exists.byFields generates correctly (SQLite)") + fun existsByFieldsSQLite() { + Configuration.connectionString = lite + assertEquals( + "SELECT EXISTS (SELECT 1 FROM $tbl WHERE data->>'it' = :test) AS it", + Query.Exists.byFields(tbl, listOf(Field.equal("it", 7, ":test"))), + "Exists query not constructed correctly" + ) + } + + @Test + @DisplayName("Exists.byContains generates correctly (PostgreSQL)") + fun existsByContainsPostgres() { + Configuration.connectionString = pg + assertEquals( + "SELECT EXISTS (SELECT 1 FROM $tbl WHERE data @> :criteria) AS it", Query.Exists.byContains(tbl), + "Exists query not constructed correctly" + ) + } + + @Test + @DisplayName("Exists.byContains fails (SQLite)") + fun existsByContainsSQLite() { + Configuration.connectionString = lite + assertThrows { Query.Exists.byContains(tbl) } + } + + @Test + @DisplayName("Exists.byJsonPath generates correctly (PostgreSQL)") + fun existsByJsonPathPostgres() { + Configuration.connectionString = pg + assertEquals( + "SELECT EXISTS (SELECT 1 FROM $tbl WHERE jsonb_path_exists(data, :path::jsonpath)) AS it", + Query.Exists.byJsonPath(tbl), "Exists query not constructed correctly" + ) + } + + @Test + @DisplayName("Exists.byJsonPath fails (SQLite)") + fun existsByJsonPathSQLite() { + Configuration.connectionString = lite + assertThrows { Query.Exists.byJsonPath(tbl) } + } @Test @DisplayName("Find.all generates correctly") @@ -482,56 +612,74 @@ class QueryTest { @Test @DisplayName("orderBy generates single, no direction for PostgreSQL") fun orderBySinglePostgres() = - assertEquals(" ORDER BY data->>'TestField'", - Query.orderBy(listOf(Field.named("TestField")), Dialect.POSTGRESQL), "ORDER BY not constructed correctly") + 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() = - assertEquals(" ORDER BY data->>'TestField'", Query.orderBy(listOf(Field.named("TestField")), Dialect.SQLITE), - "ORDER BY not constructed correctly") + 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() = - assertEquals(" ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC", + 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") + Dialect.POSTGRESQL + ), + "ORDER BY not constructed correctly" + ) @Test @DisplayName("orderBy generates multiple with direction for SQLite") fun orderByMultipleSQLite() = - assertEquals(" ORDER BY data->'Nested'->'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC", + 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") + Dialect.SQLITE + ), + "ORDER BY not constructed correctly" + ) @Test @DisplayName("orderBy generates numeric ordering PostgreSQL") fun orderByNumericPostgres() = - assertEquals(" ORDER BY (data->>'Test')::numeric", - Query.orderBy(listOf(Field.named("n:Test")), Dialect.POSTGRESQL), "ORDER BY not constructed correctly") + 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() = - assertEquals(" ORDER BY data->>'Test'", Query.orderBy(listOf(Field.named("n:Test")), Dialect.SQLITE), - "ORDER BY not constructed correctly") + 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() = - assertEquals(" ORDER BY LOWER(data#>>'{Test,Field}') DESC NULLS FIRST", + 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") + "ORDER BY not constructed correctly" + ) @Test @DisplayName("orderBy generates case-insensitive ordering for SQLite") fun orderByCISQLite() = - assertEquals(" ORDER BY data->'Test'->>'Field' COLLATE NOCASE ASC NULLS LAST", + 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") + "ORDER BY not constructed correctly" + ) }