From 4d4e1d0897892ba9399cf4545425beb7b5c3a416 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 1 Mar 2025 11:35:08 -0500 Subject: [PATCH] Add exists functions --- src/integration-test/kotlin/common/Exists.kt | 63 +++++++++ .../kotlin/postgresql/DeleteIT.kt | 3 + .../kotlin/postgresql/ExistsIT.kt | 52 ++++++++ .../kotlin/sqlite/DeleteIT.kt | 7 +- .../kotlin/sqlite/ExistsIT.kt | 46 +++++++ src/main/kotlin/ConnectionExtensions.kt | 49 ++++++- src/main/kotlin/Count.kt | 4 +- src/main/kotlin/Delete.kt | 4 +- src/main/kotlin/Exists.kt | 123 ++++++++++++++++++ 9 files changed, 343 insertions(+), 8 deletions(-) create mode 100644 src/integration-test/kotlin/common/Exists.kt create mode 100644 src/integration-test/kotlin/postgresql/ExistsIT.kt create mode 100644 src/integration-test/kotlin/sqlite/ExistsIT.kt create mode 100644 src/main/kotlin/Exists.kt diff --git a/src/integration-test/kotlin/common/Exists.kt b/src/integration-test/kotlin/common/Exists.kt new file mode 100644 index 0000000..77525ff --- /dev/null +++ b/src/integration-test/kotlin/common/Exists.kt @@ -0,0 +1,63 @@ +package solutions.bitbadger.documents.common + +import solutions.bitbadger.documents.* +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * Integration tests for the `Exists` object + */ +object Exists { + + fun byIdMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertTrue("The document with ID \"three\" should exist") { db.conn.existsById(TEST_TABLE, "three") } + } + + fun byIdNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertFalse("The document with ID \"seven\" should not exist") { db.conn.existsById(TEST_TABLE, "seven") } + } + + fun byFieldsMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertTrue("Matching documents should have been found") { + db.conn.existsByFields(TEST_TABLE, listOf(Field.equal("numValue", 10))) + } + } + + fun byFieldsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertFalse("No matching documents should have been found") { + db.conn.existsByFields(TEST_TABLE, listOf(Field.equal("nothing", "none"))) + } + } + + fun byContainsMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertTrue("Matching documents should have been found") { + db.conn.existsByContains(TEST_TABLE, mapOf("value" to "purple")) + } + } + + fun byContainsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertFalse("Matching documents should not have been found") { + db.conn.existsByContains(TEST_TABLE, mapOf("value" to "violet")) + } + } + + fun byJsonPathMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertTrue("Matching documents should have been found") { + db.conn.existsByJsonPath(TEST_TABLE, "$.numValue ? (@ == 10)") + } + } + + fun byJsonPathNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertFalse("Matching documents should not have been found") { + db.conn.existsByJsonPath(TEST_TABLE, "$.numValue ? (@ == 10.1)") + } + } +} diff --git a/src/integration-test/kotlin/postgresql/DeleteIT.kt b/src/integration-test/kotlin/postgresql/DeleteIT.kt index 6504711..0c62efe 100644 --- a/src/integration-test/kotlin/postgresql/DeleteIT.kt +++ b/src/integration-test/kotlin/postgresql/DeleteIT.kt @@ -4,6 +4,9 @@ import org.junit.jupiter.api.DisplayName import solutions.bitbadger.documents.common.Delete import kotlin.test.Test +/** + * PostgreSQL integration tests for the `Delete` object / `deleteBy*` connection extension functions + */ @DisplayName("PostgreSQL - Delete") class DeleteIT { diff --git a/src/integration-test/kotlin/postgresql/ExistsIT.kt b/src/integration-test/kotlin/postgresql/ExistsIT.kt new file mode 100644 index 0000000..3ce135d --- /dev/null +++ b/src/integration-test/kotlin/postgresql/ExistsIT.kt @@ -0,0 +1,52 @@ +package solutions.bitbadger.documents.postgresql + +import org.junit.jupiter.api.DisplayName +import solutions.bitbadger.documents.common.Exists +import kotlin.test.Test + +/** + * PostgreSQL integration tests for the `Exists` object / `existsBy*` connection extension functions + */ +@DisplayName("PostgreSQL - Exists") +class ExistsIT { + + @Test + @DisplayName("byId returns true when a document matches the ID") + fun byIdMatch() = + PgDB().use(Exists::byIdMatch) + + @Test + @DisplayName("byId returns false when no document matches the ID") + fun byIdNoMatch() = + PgDB().use(Exists::byIdNoMatch) + + @Test + @DisplayName("byFields returns true when documents match") + fun byFieldsMatch() = + PgDB().use(Exists::byFieldsMatch) + + @Test + @DisplayName("byFields returns false when no documents match") + fun byFieldsNoMatch() = + PgDB().use(Exists::byFieldsNoMatch) + + @Test + @DisplayName("byContains returns true when documents match") + fun byContainsMatch() = + PgDB().use(Exists::byContainsMatch) + + @Test + @DisplayName("byContains returns false when no documents match") + fun byContainsNoMatch() = + PgDB().use(Exists::byContainsNoMatch) + + @Test + @DisplayName("byJsonPath returns true when documents match") + fun byJsonPathMatch() = + PgDB().use(Exists::byJsonPathMatch) + + @Test + @DisplayName("byJsonPath returns false when no documents match") + fun byJsonPathNoMatch() = + PgDB().use(Exists::byJsonPathNoMatch) +} diff --git a/src/integration-test/kotlin/sqlite/DeleteIT.kt b/src/integration-test/kotlin/sqlite/DeleteIT.kt index 3a08270..68649b9 100644 --- a/src/integration-test/kotlin/sqlite/DeleteIT.kt +++ b/src/integration-test/kotlin/sqlite/DeleteIT.kt @@ -6,6 +6,9 @@ import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.common.Delete import kotlin.test.Test +/** + * SQLite integration tests for the `Delete` object / `deleteBy*` connection extension functions + */ @DisplayName("SQLite - Delete") class DeleteIT { @@ -31,13 +34,13 @@ class DeleteIT { @Test @DisplayName("byContains fails") - fun byContainsMatch() { + fun byContainsFails() { assertThrows { SQLiteDB().use(Delete::byContainsMatch) } } @Test @DisplayName("byJsonPath fails") - fun byJsonPathMatch() { + fun byJsonPathFails() { assertThrows { SQLiteDB().use(Delete::byJsonPathMatch) } } } diff --git a/src/integration-test/kotlin/sqlite/ExistsIT.kt b/src/integration-test/kotlin/sqlite/ExistsIT.kt new file mode 100644 index 0000000..87c56c7 --- /dev/null +++ b/src/integration-test/kotlin/sqlite/ExistsIT.kt @@ -0,0 +1,46 @@ +package solutions.bitbadger.documents.sqlite + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.common.Exists +import kotlin.test.Test + +/** + * SQLite integration tests for the `Exists` object / `existsBy*` connection extension functions + */ +@DisplayName("SQLite - Exists") +class ExistsIT { + + @Test + @DisplayName("byId returns true when a document matches the ID") + fun byIdMatch() = + SQLiteDB().use(Exists::byIdMatch) + + @Test + @DisplayName("byId returns false when no document matches the ID") + fun byIdNoMatch() = + SQLiteDB().use(Exists::byIdNoMatch) + + @Test + @DisplayName("byFields returns true when documents match") + fun byFieldsMatch() = + SQLiteDB().use(Exists::byFieldsMatch) + + @Test + @DisplayName("byFields returns false when no documents match") + fun byFieldsNoMatch() = + SQLiteDB().use(Exists::byFieldsNoMatch) + + @Test + @DisplayName("byContains fails") + fun byContainsFails() { + assertThrows { SQLiteDB().use(Exists::byContainsMatch) } + } + + @Test + @DisplayName("byJsonPath fails") + fun byJsonPathFails() { + assertThrows { SQLiteDB().use(Exists::byJsonPathMatch) } + } +} diff --git a/src/main/kotlin/ConnectionExtensions.kt b/src/main/kotlin/ConnectionExtensions.kt index b347437..f46dde1 100644 --- a/src/main/kotlin/ConnectionExtensions.kt +++ b/src/main/kotlin/ConnectionExtensions.kt @@ -127,7 +127,7 @@ inline fun Connection.countByContains(tableName: String, criteria: T Count.byContains(tableName, criteria, this) /** - * Count documents using a JSON containment query (PostgreSQL only) + * Count documents using a JSON Path match query (PostgreSQL only) * * @param tableName The name of the table in which documents should be counted * @param path The JSON path comparison to match @@ -137,6 +137,51 @@ inline fun Connection.countByContains(tableName: String, criteria: T fun Connection.countByJsonPath(tableName: String, path: String) = Count.byJsonPath(tableName, path, this) +// ~~~ DOCUMENT EXISTENCE QUERIES ~~~ + +/** + * Determine a document's existence by its ID + * + * @param tableName The name of the table in which document existence should be checked + * @param docId The ID of the document to be checked + * @return True if the document exists, false if not + */ +fun Connection.existsById(tableName: String, docId: TKey) = + Exists.byId(tableName, docId, this) + +/** + * Determine document existence using a field comparison + * + * @param tableName The name of the table in which document existence should be checked + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @return True if any matching documents exist, false if not + */ +fun Connection.existsByFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = + Exists.byFields(tableName, fields, howMatched, this) + +/** + * Determine document existence using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which document existence should be checked + * @param criteria The object for which JSON containment should be checked + * @return True if any matching documents exist, false if not + * @throws DocumentException If called on a SQLite connection + */ +inline fun Connection.existsByContains(tableName: String, criteria: T) = + Exists.byContains(tableName, criteria, this) + +/** + * Determine document existence using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which document existence should be checked + * @param path The JSON path comparison to match + * @return True if any matching documents exist, false if not + * @throws DocumentException If called on a SQLite connection + */ +fun Connection.existsByJsonPath(tableName: String, path: String) = + Exists.byJsonPath(tableName, path, this) + // ~~~ DOCUMENT RETRIEVAL QUERIES ~~~ /** @@ -189,7 +234,7 @@ inline fun Connection.deleteByContains(tableName: String, criteria: Delete.byContains(tableName, criteria, this) /** - * Delete documents using a JSON containment query (PostgreSQL only) + * Delete documents using a JSON Path match query (PostgreSQL only) * * @param tableName The name of the table from which documents should be deleted * @param path The JSON path comparison to match diff --git a/src/main/kotlin/Count.kt b/src/main/kotlin/Count.kt index 2a3433a..738c5c4 100644 --- a/src/main/kotlin/Count.kt +++ b/src/main/kotlin/Count.kt @@ -85,7 +85,7 @@ object Count { Configuration.dbConn().use { byContains(tableName, criteria, it) } /** - * Count documents using a JSON containment query (PostgreSQL only) + * Count documents using a JSON Path match query (PostgreSQL only) * * @param tableName The name of the table in which documents should be counted * @param path The JSON path comparison to match @@ -101,7 +101,7 @@ object Count { ) /** - * Count documents using a JSON containment query (PostgreSQL only) + * Count documents using a JSON Path match query (PostgreSQL only) * * @param tableName The name of the table in which documents should be counted * @param path The JSON path comparison to match diff --git a/src/main/kotlin/Delete.kt b/src/main/kotlin/Delete.kt index 6d8f771..d64321b 100644 --- a/src/main/kotlin/Delete.kt +++ b/src/main/kotlin/Delete.kt @@ -75,7 +75,7 @@ object Delete { Configuration.dbConn().use { byContains(tableName, criteria, it) } /** - * Delete documents using a JSON containment query (PostgreSQL only) + * Delete documents using a JSON Path match query (PostgreSQL only) * * @param tableName The name of the table from which documents should be deleted * @param path The JSON path comparison to match @@ -86,7 +86,7 @@ object Delete { conn.customNonQuery(Delete.byJsonPath(tableName), listOf(Parameter(":path", ParameterType.STRING, path))) /** - * Delete documents using a JSON containment query (PostgreSQL only) + * Delete documents using a JSON Path match query (PostgreSQL only) * * @param tableName The name of the table from which documents should be deleted * @param path The JSON path comparison to match diff --git a/src/main/kotlin/Exists.kt b/src/main/kotlin/Exists.kt new file mode 100644 index 0000000..3978f53 --- /dev/null +++ b/src/main/kotlin/Exists.kt @@ -0,0 +1,123 @@ +package solutions.bitbadger.documents + +import solutions.bitbadger.documents.query.Exists +import java.sql.Connection + +/** + * Functions to determine whether documents exist + */ +object Exists { + + /** + * Determine a document's existence by its ID + * + * @param tableName The name of the table in which document existence should be checked + * @param docId The ID of the document to be checked + * @param conn The connection on which the existence check should be executed + * @return True if the document exists, false if not + */ + fun byId(tableName: String, docId: TKey, conn: Connection) = + conn.customScalar( + Exists.byId(tableName, docId), + Parameters.addFields(listOf(Field.equal(Configuration.idField, docId, ":id"))), + Results::toExists + ) + + /** + * Determine a document's existence by its ID + * + * @param tableName The name of the table in which document existence should be checked + * @param docId The ID of the document to be checked + * @return True if the document exists, false if not + */ + fun byId(tableName: String, docId: TKey) = + Configuration.dbConn().use { byId(tableName, docId, it) } + + /** + * Determine document existence using a field comparison + * + * @param tableName The name of the table in which document existence should be checked + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param conn The connection on which the existence check should be executed + * @return True if any matching documents exist, false if not + */ + fun byFields( + tableName: String, + fields: Collection>, + howMatched: FieldMatch? = null, + conn: Connection + ): Boolean { + val named = Parameters.nameFields(fields) + return conn.customScalar( + Exists.byFields(tableName, named, howMatched), + Parameters.addFields(named), + Results::toExists + ) + } + + /** + * Determine document existence using a field comparison + * + * @param tableName The name of the table in which document existence should be checked + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @return True if any matching documents exist, false if not + */ + fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = + Configuration.dbConn().use { byFields(tableName, fields, howMatched, it) } + + /** + * Determine document existence using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which document existence should be checked + * @param criteria The object for which JSON containment should be checked + * @param conn The connection on which the existence check should be executed + * @return True if any matching documents exist, false if not + * @throws DocumentException If called on a SQLite connection + */ + inline fun byContains(tableName: String, criteria: T, conn: Connection) = + conn.customScalar( + Exists.byContains(tableName), + listOf(Parameters.json(":criteria", criteria)), + Results::toExists + ) + + /** + * Determine document existence using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which document existence should be checked + * @param criteria The object for which JSON containment should be checked + * @return True if any matching documents exist, false if not + * @throws DocumentException If called on a SQLite connection + */ + inline fun byContains(tableName: String, criteria: T) = + Configuration.dbConn().use { byContains(tableName, criteria, it) } + + /** + * Determine document existence using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which document existence should be checked + * @param path The JSON path comparison to match + * @param conn The connection on which the existence check should be executed + * @return True if any matching documents exist, false if not + * @throws DocumentException If called on a SQLite connection + */ + fun byJsonPath(tableName: String, path: String, conn: Connection) = + conn.customScalar( + Exists.byJsonPath(tableName), + listOf(Parameter(":path", ParameterType.STRING, path)), + Results::toExists + ) + + /** + * Determine document existence using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which document existence should be checked + * @param path The JSON path comparison to match + * @return True if any matching documents exist, false if not + * @throws DocumentException If called on a SQLite connection + */ + fun byJsonPath(tableName: String, path: String) = + Configuration.dbConn().use { byJsonPath(tableName, path, it) } +}