diff --git a/src/main/kotlin/Parameters.kt b/src/main/kotlin/Parameters.kt index cf54774..29dc500 100644 --- a/src/main/kotlin/Parameters.kt +++ b/src/main/kotlin/Parameters.kt @@ -98,4 +98,23 @@ object Parameters { throw DocumentException("Error creating query / binding parameters: ${ex.message}", ex) } } + + /** + * Create parameters for field names to be removed from a document + * + * @param names The names of the fields to be removed + * @param parameterName The parameter name to use for the query + * @return A list of parameters to use for building the query + */ + fun fieldNames(names: Collection, parameterName: String = ":name") = + when (Configuration.dialect("generate field name parameters")) { + Dialect.POSTGRESQL -> listOf(Parameter(parameterName, ParameterType.STRING, if (names.size == 1) { + names.elementAt(0) + } else { + names.joinToString(",").let { "{$it}" } + })) + Dialect.SQLITE -> names.mapIndexed { index, name -> + Parameter("$parameterName$index", ParameterType.STRING, name) + } + } } diff --git a/src/main/kotlin/query/Patch.kt b/src/main/kotlin/query/Patch.kt index 60d2dbf..dc1e699 100644 --- a/src/main/kotlin/query/Patch.kt +++ b/src/main/kotlin/query/Patch.kt @@ -51,7 +51,7 @@ object Patch { * @param tableName The name of the table where the document is stored * @return A query to patch JSON documents by JSON containment */ - fun byContains(tableName: String) = + fun byContains(tableName: String) = statementWhere(patch(tableName), Where.jsonContains()) /** @@ -60,6 +60,6 @@ object Patch { * @param tableName The name of the table where the document is stored * @return A query to patch JSON documents by JSON path match */ - fun byJsonPath(tableName: String) = + fun byJsonPath(tableName: String) = statementWhere(patch(tableName), Where.jsonPathMatches()) } diff --git a/src/main/kotlin/query/RemoveFields.kt b/src/main/kotlin/query/RemoveFields.kt new file mode 100644 index 0000000..1077e78 --- /dev/null +++ b/src/main/kotlin/query/RemoveFields.kt @@ -0,0 +1,74 @@ +package solutions.bitbadger.documents.query + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.query.byFields as byFieldsBase +import solutions.bitbadger.documents.query.byId as byIdBase + +/** + * Functions to create queries to remove fields from documents + */ +object RemoveFields { + + /** + * Create a query to remove fields based on the given parameters + * + * @param tableName The name of the table in which documents should have fields removed + * @param toRemove The parameters for the fields to be removed + * @return A query to remove fields from documents in the given table + */ + private fun removeFields(tableName: String, toRemove: List>) = + when (Configuration.dialect("generate field removal query")) { + Dialect.POSTGRESQL -> "UPDATE $tableName SET data = data - ${toRemove[0].name}::text[]" + Dialect.SQLITE -> toRemove.joinToString(", ") { it.name }.let { + "UPDATE $tableName SET data = json_remove(data, $it)" + } + } + + /** + * A query to patch (partially update) a JSON document by its ID + * + * @param tableName The name of the table where the document is stored + * @param toRemove The parameters for the fields to be removed + * @param docId The ID of the document to be updated (optional, used for type checking) + * @return A query to patch a JSON document by its ID + */ + fun byId(tableName: String, toRemove: List>, docId: TKey? = null) = + byIdBase(removeFields(tableName, toRemove), docId) + + /** + * A query to patch (partially update) a JSON document using field match criteria + * + * @param tableName The name of the table where the documents are stored + * @param toRemove The parameters for the fields to be removed + * @param fields The field criteria + * @param howMatched How the fields should be matched (optional, defaults to `ALL`) + * @return A query to patch JSON documents by field match criteria + */ + fun byFields( + tableName: String, + toRemove: List>, + fields: Collection>, + howMatched: FieldMatch? = null + ) = + byFieldsBase(removeFields(tableName, toRemove), fields, howMatched) + + /** + * A query to patch (partially update) a JSON document by JSON containment (PostgreSQL only) + * + * @param tableName The name of the table where the document is stored + * @param toRemove The parameters for the fields to be removed + * @return A query to patch JSON documents by JSON containment + */ + fun byContains(tableName: String, toRemove: List>) = + statementWhere(removeFields(tableName, toRemove), Where.jsonContains()) + + /** + * A query to patch (partially update) a JSON document by JSON path match (PostgreSQL only) + * + * @param tableName The name of the table where the document is stored + * @param toRemove The parameters for the fields to be removed + * @return A query to patch JSON documents by JSON path match + */ + fun byJsonPath(tableName: String, toRemove: List>) = + statementWhere(removeFields(tableName, toRemove), Where.jsonPathMatches()) +} diff --git a/src/test/kotlin/ParametersTest.kt b/src/test/kotlin/ParametersTest.kt index 293fac2..385caa6 100644 --- a/src/test/kotlin/ParametersTest.kt +++ b/src/test/kotlin/ParametersTest.kt @@ -1,5 +1,6 @@ package solutions.bitbadger.documents +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName import kotlin.test.Test import kotlin.test.assertEquals @@ -12,6 +13,14 @@ import kotlin.test.assertSame @DisplayName("Parameters") class ParametersTest { + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + fun cleanUp() { + Configuration.dialectValue = null + } + @Test @DisplayName("nameFields works with no changes") fun nameFieldsNoChange() { @@ -47,4 +56,54 @@ class ParametersTest { assertEquals("SELECT data, data_ext FROM tbl WHERE data = ? AND data_ext = ? AND more_data = ?", Parameters.replaceNamesInQuery(query, parameters), "Parameters not replaced correctly") } + + @Test + @DisplayName("fieldNames generates a single parameter (PostgreSQL)") + fun fieldNamesSinglePostgres() { + Configuration.dialectValue = Dialect.POSTGRESQL + val nameParams = Parameters.fieldNames(listOf("test")) + assertEquals(1, nameParams.size, "There should be one name parameter") + assertEquals(":name", nameParams[0].name, "The parameter name is incorrect") + assertEquals(ParameterType.STRING, nameParams[0].type, "The parameter type is incorrect") + assertEquals("test", nameParams[0].value, "The parameter value is incorrect") + } + + @Test + @DisplayName("fieldNames generates multiple parameters (PostgreSQL)") + fun fieldNamesMultiplePostgres() { + Configuration.dialectValue = Dialect.POSTGRESQL + val nameParams = Parameters.fieldNames(listOf("test", "this", "today")) + assertEquals(1, nameParams.size, "There should be one name parameter") + assertEquals(":name", nameParams[0].name, "The parameter name is incorrect") + assertEquals(ParameterType.STRING, nameParams[0].type, "The parameter type is incorrect") + assertEquals("{test,this,today}", nameParams[0].value, "The parameter value is incorrect") + } + + @Test + @DisplayName("fieldNames generates a single parameter (SQLite)") + fun fieldNamesSingleSQLite() { + Configuration.dialectValue = Dialect.SQLITE + val nameParams = Parameters.fieldNames(listOf("test")) + assertEquals(1, nameParams.size, "There should be one name parameter") + assertEquals(":name0", nameParams[0].name, "The parameter name is incorrect") + assertEquals(ParameterType.STRING, nameParams[0].type, "The parameter type is incorrect") + assertEquals("test", nameParams[0].value, "The parameter value is incorrect") + } + + @Test + @DisplayName("fieldNames generates multiple parameters (SQLite)") + fun fieldNamesMultipleSQLite() { + Configuration.dialectValue = Dialect.SQLITE + val nameParams = Parameters.fieldNames(listOf("test", "this", "today")) + assertEquals(3, nameParams.size, "There should be one name parameter") + assertEquals(":name0", nameParams[0].name, "The first parameter name is incorrect") + assertEquals(ParameterType.STRING, nameParams[0].type, "The first parameter type is incorrect") + assertEquals("test", nameParams[0].value, "The first parameter value is incorrect") + assertEquals(":name1", nameParams[1].name, "The second parameter name is incorrect") + assertEquals(ParameterType.STRING, nameParams[1].type, "The second parameter type is incorrect") + assertEquals("this", nameParams[1].value, "The second parameter value is incorrect") + assertEquals(":name2", nameParams[2].name, "The third parameter name is incorrect") + assertEquals(ParameterType.STRING, nameParams[2].type, "The third parameter type is incorrect") + assertEquals("today", nameParams[2].value, "The third parameter value is incorrect") + } } diff --git a/src/test/kotlin/query/DeleteTest.kt b/src/test/kotlin/query/DeleteTest.kt new file mode 100644 index 0000000..1fed573 --- /dev/null +++ b/src/test/kotlin/query/DeleteTest.kt @@ -0,0 +1,102 @@ +package solutions.bitbadger.documents.query + +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 solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Dialect +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Field +import kotlin.test.assertEquals + +/** + * Unit tests for the `Delete` object + */ +@DisplayName("Delete (Query)") +class DeleteTest { + + /** Test table name */ + private val tbl = "test_table" + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + fun cleanUp() { + Configuration.dialectValue = null + } + + @Test + @DisplayName("byId generates correctly (PostgreSQL)") + fun byIdPostgres() { + Configuration.dialectValue = Dialect.POSTGRESQL + assertEquals( + "DELETE FROM $tbl WHERE data->>'id' = :id", + Delete.byId(tbl), "Delete query not constructed correctly" + ) + } + + @Test + @DisplayName("byId generates correctly (SQLite)") + fun byIdSQLite() { + Configuration.dialectValue = Dialect.SQLITE + assertEquals( + "DELETE FROM $tbl WHERE data->>'id' = :id", + Delete.byId(tbl), "Delete query not constructed correctly" + ) + } + + @Test + @DisplayName("byFields generates correctly (PostgreSQL)") + fun byFieldsPostgres() { + Configuration.dialectValue = Dialect.POSTGRESQL + assertEquals( + "DELETE FROM $tbl WHERE data->>'a' = :b", Delete.byFields(tbl, listOf(Field.equal("a", "", ":b"))), + "Delete query not constructed correctly" + ) + } + + @Test + @DisplayName("byFields generates correctly (SQLite)") + fun byFieldsSQLite() { + Configuration.dialectValue = Dialect.SQLITE + assertEquals( + "DELETE FROM $tbl WHERE data->>'a' = :b", Delete.byFields(tbl, listOf(Field.equal("a", "", ":b"))), + "Delete query not constructed correctly" + ) + } + + @Test + @DisplayName("byContains generates correctly (PostgreSQL)") + fun byContainsPostgres() { + Configuration.dialectValue = Dialect.POSTGRESQL + assertEquals( + "DELETE FROM $tbl WHERE data @> :criteria", Delete.byContains(tbl), "Delete query not constructed correctly" + ) + } + + @Test + @DisplayName("byContains fails (SQLite)") + fun byContainsSQLite() { + Configuration.dialectValue = Dialect.SQLITE + assertThrows { Delete.byContains(tbl) } + } + + @Test + @DisplayName("byJsonPath generates correctly (PostgreSQL)") + fun byJsonPathPostgres() { + Configuration.dialectValue = Dialect.POSTGRESQL + assertEquals( + "DELETE FROM $tbl WHERE jsonb_path_exists(data, :path::jsonpath)", Delete.byJsonPath(tbl), + "Delete query not constructed correctly" + ) + } + + @Test + @DisplayName("byJsonPath fails (SQLite)") + fun byJsonPathSQLite() { + Configuration.dialectValue = Dialect.SQLITE + assertThrows { Delete.byJsonPath(tbl) } + } +} diff --git a/src/test/kotlin/query/FindTest.kt b/src/test/kotlin/query/FindTest.kt index ca44121..0526fa3 100644 --- a/src/test/kotlin/query/FindTest.kt +++ b/src/test/kotlin/query/FindTest.kt @@ -11,7 +11,7 @@ import solutions.bitbadger.documents.Field import kotlin.test.assertEquals /** - * Unit tests for the `Exists` object + * Unit tests for the `Find` object */ @DisplayName("Find (Query)") class FindTest {