From ee4f25aec0795e8bb6f707165ab30686499c7a4c Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 21 Feb 2025 22:59:02 -0500 Subject: [PATCH] WIP on tests and query migration --- src/main/kotlin/Query.kt | 229 ++++++++++++++++++++++++++++++----- src/test/kotlin/QueryTest.kt | 76 +++++++++--- 2 files changed, 262 insertions(+), 43 deletions(-) diff --git a/src/main/kotlin/Query.kt b/src/main/kotlin/Query.kt index bb4774f..bd58dcf 100644 --- a/src/main/kotlin/Query.kt +++ b/src/main/kotlin/Query.kt @@ -96,7 +96,7 @@ object Query { * @param dataType The type of data for the column (`JSON`, `JSONB`, etc.) * @return A query to create a document table */ - fun ensureTableFor(tableName: String, dataType: String): String = + fun ensureTableFor(tableName: String, dataType: String) = "CREATE TABLE IF NOT EXISTS $tableName (data $dataType NOT NULL)" /** @@ -146,7 +146,7 @@ object Query { * @param dialect The SQL dialect to use when creating this index * @return A query to create the key index */ - fun ensureKey(tableName: String, dialect: Dialect): String = + fun ensureKey(tableName: String, dialect: Dialect) = ensureIndexOn(tableName, "key", listOf(Configuration.idField), dialect).replace("INDEX", "UNIQUE INDEX") } @@ -184,37 +184,162 @@ object Query { * @param tableName The table into which to save (may include schema) * @return A query to save a document */ - fun save(tableName: String): String = + fun save(tableName: String) = insert(tableName, AutoId.DISABLED) + " ON CONFLICT ((data->>'${Configuration.idField}')) DO UPDATE SET data = EXCLUDED.data" /** - * Query to count documents in a table (this query has no `WHERE` clause) - * - * @param tableName The table in which to count documents (may include schema) - * @return A query to count documents + * Functions to count documents */ - fun count(tableName: String): String = - "SELECT COUNT(*) AS it FROM $tableName" + object Count { + + /** + * Query to count all documents in a table + * + * @param tableName The table in which to count documents (may include schema) + * @return A query to count documents + */ + fun all(tableName: String) = + "SELECT COUNT(*) AS it FROM $tableName" + + /** + * Query to count documents matching the given fields + * + * @param tableName The table in which to count documents (may include schema) + * @param fields The field comparisons for the count + * @param howMatched How fields should be compared (optional, defaults to ALL) + * @return A query to count documents matching the given fields + */ + fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = + statementWhere(all(tableName), Where.byFields(fields, howMatched)) + + /** + * Query to count documents via JSON containment (PostgreSQL only) + * + * @param tableName The table in which to count documents (may include schema) + * @return A query to count documents via JSON containment + */ + fun byContains(tableName: String) = + statementWhere(all(tableName), Where.jsonContains()) + + /** + * Query to count documents via a JSON path match (PostgreSQL only) + * + * @param tableName The table in which to count documents (may include schema) + * @return A query to count documents via a JSON path match + */ + fun byJsonPath(tableName: String) = + statementWhere(all(tableName), Where.jsonPathMatches()) + } /** - * Query to check for document existence in a table - * - * @param tableName The table in which existence should be checked (may include schema) - * @param where The `WHERE` clause with the existence criteria - * @return A query to check document existence + * Functions to check for document existence */ - fun exists(tableName: String, where: String): String = - "SELECT EXISTS (SELECT 1 FROM $tableName WHERE $where) AS it" + object Exists { + + /** + * Query to check for document existence in a table + * + * @param tableName The table in which existence should be checked (may include schema) + * @param where The `WHERE` clause with the existence criteria + * @return A query to check document existence + */ + private fun exists(tableName: String, where: String) = + "SELECT EXISTS (SELECT 1 FROM $tableName WHERE $where) AS it" + + /** + * Query to check for document existence by ID + * + * @param tableName The table in which existence should be checked (may include schema) + * @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?) = + exists(tableName, Where.byId(docId = docId)) + + /** + * Query to check for document existence matching the given fields + * + * @param tableName The table in which existence should be checked (may include schema) + * @param fields The field comparisons for the existence check + * @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?) = + exists(tableName, Where.byFields(fields, howMatched)) + + /** + * Query to check for document existence via JSON containment (PostgreSQL only) + * + * @param tableName The table in which existence should be checked (may include schema) + * @return A query to determine document existence via JSON containment + */ + fun byContains(tableName: String) = + exists(tableName, Where.jsonContains()) + + /** + * Query to check for document existence via a JSON path match (PostgreSQL only) + * + * @param tableName The table in which existence should be checked (may include schema) + * @return A query to determine document existence via a JSON path match + */ + fun byJsonPath(tableName: String) = + exists(tableName, Where.jsonPathMatches()) + } /** - * Query to select documents from a table (this query has no `WHERE` clause) - * - * @param tableName The table from which documents should be found (may include schema) - * @return A query to retrieve documents + * Functions to retrieve documents */ - fun find(tableName: String): String = - "SELECT data FROM $tableName" + object Find { + + /** + * Query to retrieve all documents from a table + * + * @param tableName The table from which documents should be retrieved (may include schema) + * @return A query to retrieve documents + */ + fun all(tableName: String) = + "SELECT data FROM $tableName" + + /** + * Query to retrieve a document by its ID + * + * @param tableName The table from which documents should be retrieved (may include schema) + * @param docId The ID of the document (optional, used for type checking) + * @return A query to retrieve a document by its ID + */ + fun byId(tableName: String, docId: TKey?) = + statementWhere(all(tableName), Where.byId(docId = docId)) + + /** + * Query to retrieve documents matching the given fields + * + * @param tableName The table from which documents should be retrieved (may include schema) + * @param fields The field comparisons for matching documents to retrieve + * @param howMatched How fields should be compared (optional, defaults to ALL) + * @return A query to retrieve documents matching the given fields + */ + fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch?) = + statementWhere(all(tableName), Where.byFields(fields, howMatched)) + + /** + * Query to retrieve documents via JSON containment (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved (may include schema) + * @return A query to retrieve documents via JSON containment + */ + fun byContains(tableName: String) = + statementWhere(all(tableName), Where.jsonContains()) + + /** + * Query to retrieve documents via a JSON path match (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved (may include schema) + * @return A query to retrieve documents via a JSON path match + */ + fun byJsonPath(tableName: String) = + statementWhere(all(tableName), Where.jsonPathMatches()) + } /** * Query to update (replace) a document (this query has no `WHERE` clause) @@ -222,7 +347,7 @@ object Query { * @param tableName The table in which documents should be replaced (may include schema) * @return A query to update documents */ - fun update(tableName: String): String = + fun update(tableName: String) = "UPDATE $tableName SET data = :data" /** @@ -286,13 +411,59 @@ object Query { } /** - * Query to delete documents from a table (this query has no `WHERE` clause) - * - * @param tableName The table in which documents should be deleted (may include schema) - * @return A query to delete documents + * Functions to delete documents */ - fun delete(tableName: String): String = - "DELETE FROM $tableName" + object Delete { + + /** + * Query to delete documents from a table + * + * @param tableName The table in which documents should be deleted (may include schema) + * @param where The WHERE clause for the delete statement + * @return A query to delete documents + */ + private fun delete(tableName: String, where: String) = + statementWhere("DELETE FROM $tableName", where) + + /** + * Query to delete a document by its ID + * + * @param tableName The table from which documents should be deleted (may include schema) + * @param docId The ID of the document (optional, used for type checking) + * @return A query to delete a document by its ID + */ + fun byId(tableName: String, docId: TKey?) = + delete(tableName, Where.byId(docId = docId)) + + /** + * Query to delete documents matching the given fields + * + * @param tableName The table from which documents should be deleted (may include schema) + * @param fields The field comparisons for documents to be deleted + * @param howMatched How fields should be compared (optional, defaults to ALL) + * @return A query to delete documents matching for the given fields + */ + fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch?) = + delete(tableName, Where.byFields(fields, howMatched)) + + /** + * Query to delete documents via JSON containment (PostgreSQL only) + * + * @param tableName The table from which documents should be deleted (may include schema) + * @return A query to delete documents via JSON containment + */ + fun byContains(tableName: String) = + delete(tableName, Where.jsonContains()) + + /** + * Query to delete documents via a JSON path match (PostgreSQL only) + * + * @param tableName The table from which documents should be deleted (may include schema) + * @return A query to delete documents via a JSON path match + */ + fun byJsonPath(tableName: String) = + delete(tableName, Where.jsonPathMatches()) + } /** * Create an `ORDER BY` clause for the given fields diff --git a/src/test/kotlin/QueryTest.kt b/src/test/kotlin/QueryTest.kt index d8f6233..d257501 100644 --- a/src/test/kotlin/QueryTest.kt +++ b/src/test/kotlin/QueryTest.kt @@ -399,30 +399,78 @@ class QueryTest { } @Test - @DisplayName("count generates correctly") - fun count() = - assertEquals("SELECT COUNT(*) AS it FROM $tbl", Query.count(tbl), "Count query not constructed correctly") + @DisplayName("Count.all generates correctly") + fun countAll() = + assertEquals("SELECT COUNT(*) AS it FROM $tbl", Query.Count.all(tbl), "Count query not constructed correctly") @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") + @DisplayName("Count.byFields generates correctly (PostgreSQL)") + fun countByFieldsPostgres() { + Configuration.connectionString = pg + 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") + } @Test - @DisplayName("find generates correctly") - fun find() = - assertEquals("SELECT data FROM $tbl", Query.find(tbl), "Find query not constructed correctly") + @DisplayName("Count.byFields generates correctly (PostgreSQL)") + fun countByFieldsSQLite() { + Configuration.connectionString = lite + 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") + } + + @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") + } + + @Test + @DisplayName("Count.byContains fails (SQLite)") + fun countByContainsSQLite() { + Configuration.connectionString = lite + assertThrows { Query.Count.byContains(tbl) } + } + + @Test + @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") + } + + @Test + @DisplayName("Count.byJsonPath fails (SQLite)") + fun countByJsonPathSQLite() { + Configuration.connectionString = lite + 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("Find.all generates correctly") + fun findAll() = + assertEquals("SELECT data FROM $tbl", Query.Find.all(tbl), "Find query not constructed correctly") @Test @DisplayName("update generates successfully") fun update() = assertEquals("UPDATE $tbl SET data = :data", Query.update(tbl), "Update query not constructed correctly") - @Test - @DisplayName("delete generates successfully") - fun delete() = - assertEquals("DELETE FROM $tbl", Query.delete(tbl), "Delete query not constructed correctly") +// @Test +// @DisplayName("delete generates successfully") +// fun delete() = +// assertEquals("DELETE FROM $tbl", Query.delete(tbl), "Delete query not constructed correctly") @Test @DisplayName("orderBy generates for no fields")