diff --git a/src/main/java/com/homihq/db2rest/rest/delete/DeleteController.java b/src/main/java/com/homihq/db2rest/rest/delete/DeleteController.java index 412f858a..79f7c07d 100644 --- a/src/main/java/com/homihq/db2rest/rest/delete/DeleteController.java +++ b/src/main/java/com/homihq/db2rest/rest/delete/DeleteController.java @@ -1,8 +1,10 @@ package com.homihq.db2rest.rest.delete; +import com.homihq.db2rest.rest.delete.dto.DeleteResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -14,10 +16,12 @@ public class DeleteController { private final DeleteService deleteService; @DeleteMapping("/{tableName}") - public void delete(@PathVariable String tableName, - @RequestHeader(name = "Content-Profile") String schemaName, - @RequestParam(name = "filter", required = false, defaultValue = "") String filter) { + public ResponseEntity delete(@PathVariable String tableName, + @RequestHeader(name = "Content-Profile") String schemaName, + @RequestParam(name = "filter", required = false, defaultValue = "") String filter) { - deleteService.delete(schemaName, tableName, filter); + int rows = deleteService.delete(schemaName, tableName, filter); + log.debug("Number of rows deleted - {}", rows); + return ResponseEntity.ok(DeleteResponse.builder().rows(rows).build()); } } diff --git a/src/main/java/com/homihq/db2rest/rest/delete/DeleteService.java b/src/main/java/com/homihq/db2rest/rest/delete/DeleteService.java index 99f3a860..fae2413c 100644 --- a/src/main/java/com/homihq/db2rest/rest/delete/DeleteService.java +++ b/src/main/java/com/homihq/db2rest/rest/delete/DeleteService.java @@ -2,6 +2,7 @@ import com.homihq.db2rest.config.Db2RestConfigProperties; import com.homihq.db2rest.exception.DeleteOpNotAllowedException; +import com.homihq.db2rest.exception.GenericDataAccessException; import com.homihq.db2rest.exception.InvalidTableException; import com.homihq.db2rest.mybatis.MyBatisTable; import com.homihq.db2rest.rsql.operators.SimpleRSQLOperators; @@ -17,13 +18,13 @@ import org.mybatis.dynamic.sql.delete.DeleteModel; import org.mybatis.dynamic.sql.delete.render.DeleteStatementProvider; import org.mybatis.dynamic.sql.render.RenderingStrategies; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import schemacrawler.schema.Table; -import java.util.Map; - import static org.mybatis.dynamic.sql.delete.DeleteDSL.deleteFrom; @Service @@ -33,27 +34,27 @@ public class DeleteService { private final Db2RestConfigProperties db2RestConfigProperties; + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; private final SchemaManager schemaManager; @Transactional - public void delete(String schemaName, String tableName, String filter) { + public int delete(String schemaName, String tableName, String filter) { - if(StringUtils.isBlank(filter) && db2RestConfigProperties.isAllowSafeDelete()) { + if (StringUtils.isBlank(filter) && db2RestConfigProperties.isAllowSafeDelete()) { throw new DeleteOpNotAllowedException(true); - } - else{ + } else { db2RestConfigProperties.verifySchema(schemaName); Table t = schemaManager.getTable(schemaName, tableName) .orElseThrow(() -> new InvalidTableException(tableName)); - MyBatisTable table = new MyBatisTable(schemaName, tableName, t); + var table = new MyBatisTable(schemaName, tableName, t); DeleteDSL deleteDSL = deleteFrom(table); - if(StringUtils.isNotBlank(filter)) { + if (StringUtils.isNotBlank(filter)) { Node rootNode = new RSQLParser(SimpleRSQLOperators.customOperators()).parse(filter); @@ -66,13 +67,15 @@ public void delete(String schemaName, String tableName, String filter) { DeleteStatementProvider deleteStatement = deleteDSL.build() .render(RenderingStrategies.SPRING_NAMED_PARAMETER); - String sql = deleteStatement.getDeleteStatement(); - Map bindValues = deleteStatement.getParameters(); - - - log.info("SQL - {}", sql); - log.info("Bind variables - {}", bindValues); + log.info("SQL - {}", deleteStatement.getDeleteStatement()); + log.info("Bind variables - {}", deleteStatement.getParameters()); + try { + return namedParameterJdbcTemplate.update(deleteStatement.getDeleteStatement(), + deleteStatement.getParameters()); + } catch (DataAccessException e) { + throw new GenericDataAccessException(e.getMostSpecificCause().getMessage()); + } } diff --git a/src/main/java/com/homihq/db2rest/rest/delete/dto/DeleteResponse.java b/src/main/java/com/homihq/db2rest/rest/delete/dto/DeleteResponse.java new file mode 100644 index 00000000..4beae886 --- /dev/null +++ b/src/main/java/com/homihq/db2rest/rest/delete/dto/DeleteResponse.java @@ -0,0 +1,7 @@ +package com.homihq.db2rest.rest.delete.dto; + +import lombok.Builder; + +@Builder +public record DeleteResponse(int rows) { +} diff --git a/src/test/java/com/homihq/db2rest/rest/MySQLDeleteControllerTest.java b/src/test/java/com/homihq/db2rest/rest/MySQLDeleteControllerTest.java new file mode 100644 index 00000000..b39ace9c --- /dev/null +++ b/src/test/java/com/homihq/db2rest/rest/MySQLDeleteControllerTest.java @@ -0,0 +1,79 @@ +package com.homihq.db2rest.rest; + +import com.homihq.db2rest.MySQLBaseIntegrationTest; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.Matchers.containsString; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class MySQLDeleteControllerTest extends MySQLBaseIntegrationTest { + + @Test + @DisplayName("Delete a Director") + void delete_single_record() throws Exception { + mockMvc.perform(delete("/director") + .param("filter", "first_name==\"Alex\"") + .header("Content-Profile", "sakila") + .accept(APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.rows", Matchers.equalTo(1))) + .andDo(print()) + .andDo(document("mysql-delete-a-director")); + } + + @Test + @DisplayName("Delete all records while allowSafeDelete=true") + void delete_all_records_with_allow_safe_delete_true() throws Exception { + mockMvc.perform(delete("/director") + .header("Content-Profile", "sakila") + .accept(APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.detail", + containsString("Invalid delete operation , safe set to true"))) + .andDo(print()) + .andDo(document("mysql-delete-a-director")); + } + + @Disabled + @Test + @DisplayName("Delete all records while allowSafeDelete=false") + void delete_all_records_with_allow_safe_delete_false() throws Exception { + + } + + @Test + @DisplayName("Column Does Not Exist") + void column_does_not_exist() throws Exception { + mockMvc.perform(delete("/director") + .param("filter", "_name==\"Alex\"") + .header("Content-Profile", "sakila") + .accept(APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.detail", + containsString("Unknown column '_name' in 'where clause'"))) + .andDo(print()) + .andDo(document("mysql-delete-a-director")); + } + + @Test + @DisplayName("Foreign Key Constraint Violation") + void foreign_key_constraint_violation() throws Exception { + mockMvc.perform(delete("/language") + .param("filter", "name==\"ENGLISH\"") + .header("Content-Profile", "sakila") + .accept(APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.detail", + containsString("Cannot delete or update a parent row: a foreign key constraint fails"))) + .andDo(print()) + .andDo(document("mysql-delete-a-director")); + } +} diff --git a/src/test/java/com/homihq/db2rest/rest/PgProcedureControllerTest.java b/src/test/java/com/homihq/db2rest/rest/PgProcedureControllerTest.java index 2e1bba80..9ce1b143 100644 --- a/src/test/java/com/homihq/db2rest/rest/PgProcedureControllerTest.java +++ b/src/test/java/com/homihq/db2rest/rest/PgProcedureControllerTest.java @@ -1,8 +1,6 @@ package com.homihq.db2rest.rest; import com.homihq.db2rest.PostgreSQLBaseIntegrationTest; -import org.hamcrest.Matchers; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; diff --git a/src/test/resources/application-it.yaml b/src/test/resources/application-it.yaml new file mode 100644 index 00000000..acbbe55a --- /dev/null +++ b/src/test/resources/application-it.yaml @@ -0,0 +1,4 @@ +db2rest: + schemas: + - sakila + - public \ No newline at end of file diff --git a/src/test/resources/mysql/mysql-sakila-data.sql b/src/test/resources/mysql/mysql-sakila-data.sql index de40f641..c01d0603 100644 --- a/src/test/resources/mysql/mysql-sakila-data.sql +++ b/src/test/resources/mysql/mysql-sakila-data.sql @@ -46,6 +46,29 @@ Values ('4','JENNIFER','DAVIS','2006-02-15 04:34:33.000'); +-- director + +Insert into director +(director_id,first_name,last_name,last_update) +Values + ('1','Michael','Tam','2006-02-15 04:34:33.000'); + +Insert into director +(director_id,first_name,last_name,last_update) +Values + ('2','Paul','Anderson','2006-02-15 04:34:33.000'); + +Insert into director +(director_id,first_name,last_name,last_update) +Values + ('3','Alex','Chase','2006-02-15 04:34:33.000'); + +Insert into director +(director_id,first_name,last_name,last_update) +Values + ('4','Karol','Davis','2006-02-15 04:34:33.000'); + + -- category Insert into category diff --git a/src/test/resources/pg/postgres-sakila-data.sql b/src/test/resources/pg/postgres-sakila-data.sql index 064aa0ac..4a84cc08 100644 --- a/src/test/resources/pg/postgres-sakila-data.sql +++ b/src/test/resources/pg/postgres-sakila-data.sql @@ -44,6 +44,30 @@ Insert into actor Values ('4','JENNIFER','DAVIS','2006-02-15 04:34:33.000'); + +-- director + +Insert into director +(director_id,first_name,last_name,last_update) +Values + ('1','Michael','Tam','2006-02-15 04:34:33.000'); + +Insert into director +(director_id,first_name,last_name,last_update) +Values + ('2','Paul','Anderson','2006-02-15 04:34:33.000'); + +Insert into director +(director_id,first_name,last_name,last_update) +Values + ('3','Alex','Chase','2006-02-15 04:34:33.000'); + +Insert into director +(director_id,first_name,last_name,last_update) +Values + ('4','Karol','Davis','2006-02-15 04:34:33.000'); + + -- category Insert into category