From 53018a14c855781488782faef017b7cd319d9ca9 Mon Sep 17 00:00:00 2001 From: Arnold Galovics Date: Mon, 20 Mar 2023 21:24:31 +0100 Subject: [PATCH] FINERACT-1724: Refactored datatable index and unique constraint handling + creating an index on fk columns for multirow datatables --- .../org.apache.fineract.dependencies.gradle | 2 + .../CreateDatatableCommandHandler.java | 10 +- .../CreateDatatableEntryCommandHandler.java | 10 +- .../UpdateDatatableCommandHandler.java | 8 +- .../service/DatatableKeywordGenerator.java | 33 ++ .../service/GenericDataService.java | 4 + .../service/GenericDataServiceImpl.java | 49 +-- .../ReadWriteNonCoreDataServiceImpl.java | 73 +++-- .../starter/DataQueriesAutoConfiguration.java | 6 +- integration-tests/dependencies.gradle | 6 + .../DatatableUniqueAndIndexColumnTest.java | 220 ------------- .../integrationtests/common/Utils.java | 12 + .../common/system/DatatableHelper.java | 6 + .../datatable/DatatableEntity.java | 31 ++ .../DatatableIntegrationTest.java | 2 +- .../datatable/DatatableTestNameGenerator.java | 30 ++ .../DatatableUniqueAndIndexColumnTest.java | 306 ++++++++++++++++++ 17 files changed, 515 insertions(+), 293 deletions(-) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableKeywordGenerator.java delete mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/DatatableUniqueAndIndexColumnTest.java create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/datatable/DatatableEntity.java rename integration-tests/src/test/java/org/apache/fineract/integrationtests/{ => datatable}/DatatableIntegrationTest.java (99%) create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/datatable/DatatableTestNameGenerator.java create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/datatable/DatatableUniqueAndIndexColumnTest.java diff --git a/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle b/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle index 376398548..90e45441f 100644 --- a/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle +++ b/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle @@ -233,5 +233,7 @@ dependencyManagement { } dependency 'org.postgresql:postgresql:42.5.4' + + dependency 'org.assertj:assertj-core:3.24.2' } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/CreateDatatableCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/CreateDatatableCommandHandler.java index e181fed81..972ac19cb 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/CreateDatatableCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/CreateDatatableCommandHandler.java @@ -18,28 +18,24 @@ */ package org.apache.fineract.infrastructure.dataqueries.handler; +import lombok.RequiredArgsConstructor; import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service +@RequiredArgsConstructor public class CreateDatatableCommandHandler implements NewCommandSourceHandler { private final ReadWriteNonCoreDataService writePlatformService; - @Autowired - public CreateDatatableCommandHandler(final ReadWriteNonCoreDataService writePlatformService) { - this.writePlatformService = writePlatformService; - } - @Transactional @Override public CommandProcessingResult processCommand(final JsonCommand command) { - return this.writePlatformService.createDatatable(command); + return writePlatformService.createDatatable(command); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/CreateDatatableEntryCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/CreateDatatableEntryCommandHandler.java index 15c3f9985..cfca9534b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/CreateDatatableEntryCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/CreateDatatableEntryCommandHandler.java @@ -18,28 +18,24 @@ */ package org.apache.fineract.infrastructure.dataqueries.handler; +import lombok.RequiredArgsConstructor; import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service +@RequiredArgsConstructor public class CreateDatatableEntryCommandHandler implements NewCommandSourceHandler { private final ReadWriteNonCoreDataService writePlatformService; - @Autowired - public CreateDatatableEntryCommandHandler(final ReadWriteNonCoreDataService writePlatformService) { - this.writePlatformService = writePlatformService; - } - @Transactional @Override public CommandProcessingResult processCommand(final JsonCommand command) { - return this.writePlatformService.createNewDatatableEntry(command.entityName(), command.entityId(), command); + return writePlatformService.createNewDatatableEntry(command.entityName(), command.entityId(), command); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/UpdateDatatableCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/UpdateDatatableCommandHandler.java index 8c4eddf09..9138442c7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/UpdateDatatableCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/handler/UpdateDatatableCommandHandler.java @@ -18,25 +18,21 @@ */ package org.apache.fineract.infrastructure.dataqueries.handler; +import lombok.RequiredArgsConstructor; import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service +@RequiredArgsConstructor public class UpdateDatatableCommandHandler implements NewCommandSourceHandler { private final ReadWriteNonCoreDataService writePlatformService; - @Autowired - public UpdateDatatableCommandHandler(final ReadWriteNonCoreDataService writePlatformService) { - this.writePlatformService = writePlatformService; - } - @Transactional @Override public CommandProcessingResult processCommand(final JsonCommand command) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableKeywordGenerator.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableKeywordGenerator.java new file mode 100644 index 000000000..4b200ad55 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableKeywordGenerator.java @@ -0,0 +1,33 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.dataqueries.service; + +import org.springframework.stereotype.Component; + +@Component +public class DatatableKeywordGenerator { + + public String generateIndexName(String tableName, String columnName) { + return "idx_%s_%s".formatted(tableName, columnName); + } + + public String generateUniqueKeyName(String tableName, String columnName) { + return "uk_%s_%s".formatted(tableName, columnName); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/GenericDataService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/GenericDataService.java index 6ca0bcd7c..0d239578c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/GenericDataService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/GenericDataService.java @@ -33,4 +33,8 @@ public interface GenericDataService { String wrapSQL(String sql); List fillResultsetColumnHeaders(String datatable); + + boolean isExplicitlyUnique(String datatable, String columnName); + + boolean isExplicitlyIndexed(String datatable, String columnName); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/GenericDataServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/GenericDataServiceImpl.java index 2eb6cf6f6..4c166767a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/GenericDataServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/GenericDataServiceImpl.java @@ -25,7 +25,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; -import javax.sql.DataSource; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; import org.apache.fineract.infrastructure.core.service.database.DatabaseIndependentQueryService; @@ -36,7 +36,6 @@ import org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeader import org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnValueData; import org.apache.fineract.infrastructure.dataqueries.data.ResultsetRowData; import org.apache.fineract.infrastructure.dataqueries.exception.DatatableNotFoundException; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.support.rowset.SqlRowSet; @@ -45,19 +44,13 @@ import org.springframework.stereotype.Service; @Service @Slf4j +@RequiredArgsConstructor public class GenericDataServiceImpl implements GenericDataService { private final JdbcTemplate jdbcTemplate; - private final DataSource dataSource; + private final RoutingDataSource dataSource; private final DatabaseIndependentQueryService databaseIndependentQueryService; - - @Autowired - public GenericDataServiceImpl(final RoutingDataSource dataSource, final JdbcTemplate jdbcTemplate, - DatabaseIndependentQueryService databaseIndependentQueryService) { - this.dataSource = dataSource; - this.databaseIndependentQueryService = databaseIndependentQueryService; - this.jdbcTemplate = new JdbcTemplate(this.dataSource); - } + private final DatatableKeywordGenerator datatableKeywordGenerator; @Override public GenericResultsetData fillGenericResultSet(final String sql) { @@ -228,11 +221,13 @@ public class GenericDataServiceImpl implements GenericDataService { final boolean columnNullable = "YES".equalsIgnoreCase(isNullable) || "TRUE".equalsIgnoreCase(isNullable); final boolean columnIsPrimaryKey = "PRI".equalsIgnoreCase(isPrimaryKey) || "TRUE".equalsIgnoreCase(isPrimaryKey); - final boolean columnIsUnique = checkUnique(datatable, columnName, indexDefinitions); - boolean columnIsIndexed = false; - if (!columnIsUnique) { - columnIsIndexed = checkIndexed(datatable, columnName, indexDefinitions); - } + + // primary keys are automatically unique + final boolean columnIsUnique = columnIsPrimaryKey || isExplicitlyUnique(datatable, columnName, indexDefinitions); + + // primary keys and unique constrained columns are automatically indexed + final boolean columnIsIndexed = columnIsPrimaryKey || columnIsUnique + || isExplicitlyIndexed(datatable, columnName, indexDefinitions); List columnValues = new ArrayList<>(); String codeName = null; @@ -240,7 +235,7 @@ public class GenericDataServiceImpl implements GenericDataService { if ("varchar".equalsIgnoreCase(columnType) || "int".equalsIgnoreCase(columnType) || "integer".equalsIgnoreCase(columnType)) { if (codePosition > 0) { codeName = columnName.substring(0, codePosition); - columnValues = retreiveColumnValues(codeName); + columnValues = retrieveColumnValues(codeName); } } @@ -251,16 +246,26 @@ public class GenericDataServiceImpl implements GenericDataService { return columnHeaders; } - private boolean checkUnique(String datatable, String columnName, List indexDefinitions) { - String keyNameToCheck = "uk_" + datatable + "_" + columnName; + private boolean isExplicitlyUnique(String datatable, String columnName, List indexDefinitions) { + String keyNameToCheck = datatableKeywordGenerator.generateUniqueKeyName(datatable, columnName); return checkKeyPresent(keyNameToCheck, indexDefinitions); } - private boolean checkIndexed(String datatable, String columnName, List indexDefinitions) { - String keyNameToCheck = "idx_" + datatable + "_" + columnName; + @Override + public boolean isExplicitlyUnique(String datatable, String columnName) { + return isExplicitlyUnique(datatable, columnName, getDatatableIndexData(datatable)); + } + + private boolean isExplicitlyIndexed(String datatable, String columnName, List indexDefinitions) { + String keyNameToCheck = datatableKeywordGenerator.generateIndexName(datatable, columnName); return checkKeyPresent(keyNameToCheck, indexDefinitions); } + @Override + public boolean isExplicitlyIndexed(String datatable, String columnName) { + return isExplicitlyIndexed(datatable, columnName, getDatatableIndexData(datatable)); + } + private boolean checkKeyPresent(String keyNameToCheck, List indexDefinitions) { for (IndexDetail indexDetail : indexDefinitions) { if (indexDetail.getIndexName().equals(keyNameToCheck)) { @@ -282,7 +287,7 @@ public class GenericDataServiceImpl implements GenericDataService { /* * Candidate for using caching there to get allowed 'column values' from code/codevalue tables */ - private List retreiveColumnValues(final String codeName) { + private List retrieveColumnValues(final String codeName) { final List columnValues = new ArrayList<>(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java index 17653a3ae..cbb612a7a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/ReadWriteNonCoreDataServiceImpl.java @@ -143,6 +143,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ private final ColumnValidator columnValidator; private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; private final SqlInjectionPreventerService preventSqlInjectionService; + private final DatatableKeywordGenerator datatableKeywordGenerator; @Override public List retrieveDatatableNames(final String appTable) { @@ -753,7 +754,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ } } if (unique) { - String uniqueKeyName = "uk_" + dataTableNameAlias + "_" + name; + String uniqueKeyName = datatableKeywordGenerator.generateUniqueKeyName(dataTableNameAlias, name); constrainBuilder.append(", CONSTRAINT ").append(sqlGenerator.escape(uniqueKeyName)).append(" ") .append("UNIQUE (" + sqlGenerator.escape(name) + ")"); } @@ -854,11 +855,19 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ if (databaseTypeResolver.isMySQL()) { sqlBuilder.append(" ENGINE=InnoDB DEFAULT CHARSET=UTF8MB4;"); } - log.debug("SQL:: {}", sqlBuilder.toString()); + log.debug("SQL:: {}", sqlBuilder); - this.jdbcTemplate.execute(sqlBuilder.toString()); + jdbcTemplate.execute(sqlBuilder.toString()); // create indexes + if (multiRow) { + createFkIndex(datatableName, fkColumnName); + } else { + /* + * in case of non-multirow, the primary key of the table is the FK and MySQL and PostgreSQL + * automatically puts an index onto it so no need to create it explicitly + */ + } createIndexesForTable(datatableName, columns); registerDatatable(datatableName, apptableName, entitySubType); registerColumnCodeMapping(codeMappings); @@ -884,6 +893,11 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withResourceIdAsString(datatableName).build(); } + private void createFkIndex(String datatableName, String fkColumnName) { + String indexName = datatableKeywordGenerator.generateIndexName(datatableName, fkColumnName); + createIndex(indexName, datatableName, fkColumnName); + } + private void createIndexesForTable(String datatableName, JsonArray columns) { for (final JsonElement column : columns) { createIndexForColumn(datatableName, column.getAsJsonObject()); @@ -896,8 +910,8 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ final Boolean indexed = column.has("indexed") ? column.get("indexed").getAsBoolean() : false; if (indexed) { if (!unique) { - String indexName = "idx_" + datatableName + "_" + name; - createIndexForColumnOnTable(indexName, datatableName, name); + String indexName = datatableKeywordGenerator.generateIndexName(datatableName, name); + createIndex(indexName, datatableName, name); } } } @@ -1072,7 +1086,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ } if (unique) { - String uniqueKeyName = "uk_" + dataTableNameAlias + "_" + name; + String uniqueKeyName = datatableKeywordGenerator.generateUniqueKeyName(dataTableNameAlias, name); constrainBuilder.append(",ADD CONSTRAINT ").append(sqlGenerator.escape(uniqueKeyName)).append(" ") .append("UNIQUE (" + sqlGenerator.escape(name) + ")"); } @@ -1277,7 +1291,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ sqlBuilder = sqlBuilder.deleteCharAt(indexOfFirstComma); } sqlBuilder.append(constrainBuilder); - this.jdbcTemplate.execute(sqlBuilder.toString()); + jdbcTemplate.execute(sqlBuilder.toString()); createIndexesForTable(datatableName, addColumns); registerColumnCodeMapping(codeMappings); } @@ -1302,7 +1316,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ } sqlBuilder.append(constrainBuilder); try { - this.jdbcTemplate.execute(sqlBuilder.toString()); + jdbcTemplate.execute(sqlBuilder.toString()); deleteColumnCodeMapping(removeMappings); registerColumnCodeMapping(codeMappings); // update unique constraint @@ -1310,6 +1324,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ // update indexes updateIndexesForTable(datatableName, changeColumns, mapColumnNameDefinition); } catch (final Exception e) { + log.error("Exception while modifying a datatable", e); if (e.getMessage().contains("Error on rename")) { throw new PlatformServiceUnavailableException("error.msg.datatable.column.update.not.allowed", "One of the column name modification not allowed", e); @@ -1380,8 +1395,8 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ String name = column.has("name") ? column.get("name").getAsString() : null; String columnNewName = column.has("newName") ? column.get("newName").getAsString() : null; final Boolean setUnique = column.has("unique") ? column.get("unique").getAsBoolean() : false; - final Boolean isAlreadyUnique = columnMetaData.getIsColumnUnique(); - String uniqueKeyName = "uk_" + datatableName + "_" + name; + final Boolean isAlreadyUnique = genericDataService.isExplicitlyUnique(datatableName, name); + String uniqueKeyName = datatableKeywordGenerator.generateUniqueKeyName(datatableName, name); if (isAlreadyUnique) { if (!setUnique) { @@ -1401,7 +1416,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ private void checkColumnRenameAndCreateUniqueConstraint(String datatableName, String name, String columnNewName, String constraintKey) { if (columnNewName != null) { // create constraint with new column name - String uniqueKeyName = "uk_" + datatableName + "_" + columnNewName; + String uniqueKeyName = datatableKeywordGenerator.generateUniqueKeyName(datatableName, columnNewName); createUniqueConstraint(datatableName, columnNewName, uniqueKeyName); } else { // create constraint for column @@ -1414,7 +1429,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ // drop existing constraint dropUniqueConstraint(datatableName, existingConstraint); // create constraint with new column name - String uniqueKeyName = "uk_" + datatableName + "_" + columnNewName; + String uniqueKeyName = datatableKeywordGenerator.generateUniqueKeyName(datatableName, columnNewName); createUniqueConstraint(datatableName, columnNewName, uniqueKeyName); } } @@ -1452,19 +1467,19 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ final Boolean setForUnique = column.has("unique") ? column.get("unique").getAsBoolean() : false; final Boolean setForIndexed = column.has("indexed") ? column.get("indexed").getAsBoolean() : false; if (!setForUnique) { - final Boolean isAlreadyIndexed = columnMetaData.getIsColumnIndexed(); - String uniqueIndexName = "idx_" + datatableName + "_" + name; + final Boolean isAlreadyIndexed = genericDataService.isExplicitlyIndexed(datatableName, name); + String indexName = datatableKeywordGenerator.generateIndexName(datatableName, name); if (isAlreadyIndexed) { if (!setForIndexed) { // drop index - dropIndex(datatableName, uniqueIndexName); + dropIndex(datatableName, indexName); } else { // if column name changed - checkColumnRenameAndModifyIndex(datatableName, columnNewName, uniqueIndexName); + checkColumnRenameAndModifyIndex(datatableName, columnNewName, indexName); } } else { if (setForIndexed) { - checkColumnRenameAndCreateIndex(datatableName, name, columnNewName, uniqueIndexName); + checkColumnRenameAndCreateIndex(datatableName, name, columnNewName, indexName); } } @@ -1474,12 +1489,12 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ private void checkColumnRenameAndCreateIndex(String datatableName, String columnExistingName, String columnNewName, String indexName) { if (columnNewName != null) { - String uniqueIndexName = "idx_" + datatableName + "_" + columnNewName; + String newIndexName = datatableKeywordGenerator.generateIndexName(datatableName, columnNewName); // create index with new column name - createIndexForColumnOnTable(uniqueIndexName, datatableName, columnNewName); + createIndex(newIndexName, datatableName, columnNewName); } else { // create index with previous name - createIndexForColumnOnTable(indexName, datatableName, columnExistingName); + createIndex(indexName, datatableName, columnExistingName); } } @@ -1488,16 +1503,17 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ // drop index with previous name dropIndex(datatableName, existingIndex); // create index with new name - String uniqueIndexName = "idx_" + datatableName + "_" + columnNewName; - createIndexForColumnOnTable(uniqueIndexName, datatableName, columnNewName); + String newIndexName = datatableKeywordGenerator.generateIndexName(datatableName, columnNewName); + createIndex(newIndexName, datatableName, columnNewName); } } - private void createIndexForColumnOnTable(String uniqueIndexName, String datatableName, String columnName) { - StringBuilder sqlIndexUpdateBuilder = new StringBuilder(); - sqlIndexUpdateBuilder.append("CREATE INDEX ").append(sqlGenerator.escape(uniqueIndexName)).append(" ON ") - .append(sqlGenerator.escape(datatableName)).append(" (").append(sqlGenerator.escape(columnName)).append(");"); - this.jdbcTemplate.execute(sqlIndexUpdateBuilder.toString()); + private void createIndex(String indexName, String tableName, String columnName) { + String safeIndexName = sqlGenerator.escape(indexName); + String safeTableName = sqlGenerator.escape(tableName); + String safeColumnName = sqlGenerator.escape(columnName); + String sqlIndexUpdateBuilder = "CREATE INDEX %s ON %s (%s);".formatted(safeIndexName, safeTableName, safeColumnName); + jdbcTemplate.execute(sqlIndexUpdateBuilder); } private void dropIndex(String datatableName, String uniqueIndexName) { @@ -1506,7 +1522,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ sqlIndexUpdateBuilder.append("ALTER TABLE ").append(sqlGenerator.escape(datatableName)).append(" "); } sqlIndexUpdateBuilder.append("DROP INDEX ").append(sqlGenerator.escape(uniqueIndexName)).append(";"); - this.jdbcTemplate.execute(sqlIndexUpdateBuilder.toString()); + jdbcTemplate.execute(sqlIndexUpdateBuilder.toString()); } @Transactional @@ -1662,6 +1678,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ } @Override + @Transactional(readOnly = true) public GenericResultsetData retrieveDataTableGenericResultSet(final String dataTableName, final Long appTableId, final String order, final Long id) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/starter/DataQueriesAutoConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/starter/DataQueriesAutoConfiguration.java index 7bc5b954f..c0464e9d5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/starter/DataQueriesAutoConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/starter/DataQueriesAutoConfiguration.java @@ -25,6 +25,7 @@ import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; import org.apache.fineract.infrastructure.core.service.database.DatabaseTypeResolver; import org.apache.fineract.infrastructure.dataqueries.data.DataTableValidator; +import org.apache.fineract.infrastructure.dataqueries.service.DatatableKeywordGenerator; import org.apache.fineract.infrastructure.dataqueries.service.GenericDataService; import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataService; import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataServiceImpl; @@ -48,9 +49,10 @@ public class DataQueriesAutoConfiguration { final DatatableCommandFromApiJsonDeserializer fromApiJsonDeserializer, final ConfigurationDomainService configurationDomainService, final CodeReadPlatformService codeReadPlatformService, final DataTableValidator dataTableValidator, final ColumnValidator columnValidator, - final NamedParameterJdbcTemplate namedParameterJdbcTemplate, final SqlInjectionPreventerService preventSqlInjectionService) { + final NamedParameterJdbcTemplate namedParameterJdbcTemplate, final SqlInjectionPreventerService preventSqlInjectionService, + DatatableKeywordGenerator datatableKeywordGenerator) { return new ReadWriteNonCoreDataServiceImpl(jdbcTemplate, databaseTypeResolver, sqlGenerator, context, fromJsonHelper, genericDataService, fromApiJsonDeserializer, configurationDomainService, codeReadPlatformService, dataTableValidator, - columnValidator, namedParameterJdbcTemplate, preventSqlInjectionService); + columnValidator, namedParameterJdbcTemplate, preventSqlInjectionService, datatableKeywordGenerator); } } diff --git a/integration-tests/dependencies.gradle b/integration-tests/dependencies.gradle index 85c597a8e..6404bccdc 100644 --- a/integration-tests/dependencies.gradle +++ b/integration-tests/dependencies.gradle @@ -43,5 +43,11 @@ dependencies { testCompileOnly 'org.projectlombok:lombok' testAnnotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.assertj:assertj-core' + + testImplementation 'org.mapstruct:mapstruct' + testAnnotationProcessor 'org.mapstruct:mapstruct-processor' + testImplementation 'com.github.tomakehurst:wiremock-jre8:2.35.0' + } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DatatableUniqueAndIndexColumnTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DatatableUniqueAndIndexColumnTest.java deleted file mode 100644 index 50d59dbdb..000000000 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DatatableUniqueAndIndexColumnTest.java +++ /dev/null @@ -1,220 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.fineract.integrationtests; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import io.restassured.builder.RequestSpecBuilder; -import io.restassured.builder.ResponseSpecBuilder; -import io.restassured.http.ContentType; -import io.restassured.specification.RequestSpecification; -import io.restassured.specification.ResponseSpecification; -import java.util.List; -import org.apache.fineract.client.models.GetDataTablesResponse; -import org.apache.fineract.client.models.PostColumnHeaderData; -import org.apache.fineract.client.models.PostDataTablesRequest; -import org.apache.fineract.client.models.PostDataTablesResponse; -import org.apache.fineract.client.models.PutDataTablesRequest; -import org.apache.fineract.client.models.PutDataTablesRequestAddColumns; -import org.apache.fineract.client.models.PutDataTablesRequestChangeColumns; -import org.apache.fineract.client.models.PutDataTablesResponse; -import org.apache.fineract.client.models.ResultsetColumnHeaderData; -import org.apache.fineract.integrationtests.common.Utils; -import org.apache.fineract.integrationtests.common.system.DatatableHelper; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -public class DatatableUniqueAndIndexColumnTest { - - private static final String LOAN_APP_TABLE_NAME = "m_loan"; - private RequestSpecification requestSpec; - private ResponseSpecification responseSpec; - private DatatableHelper datatableHelper; - - @BeforeEach - public void setup() { - Utils.initializeRESTAssured(); - this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); - this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); - this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); - this.datatableHelper = new DatatableHelper(this.requestSpec, this.responseSpec); - } - - @Test - public void testDatableCreationWithUniqueAndIndexedColumns() { - // create dataTable - String datatableName = Utils.uniqueRandomStringGenerator("dt_" + LOAN_APP_TABLE_NAME + "_", 5).toLowerCase().toLowerCase(); - String column1Name = "itsanumber"; - String column2Name = "itsastring"; - - PostDataTablesRequest request = new PostDataTablesRequest(); - request.setDatatableName(datatableName); - request.setApptableName(LOAN_APP_TABLE_NAME); - request.setMultiRow(false); - - PostColumnHeaderData column1HeaderRequestData = new PostColumnHeaderData(); - column1HeaderRequestData.setName(column1Name); - column1HeaderRequestData.setType("Number"); - column1HeaderRequestData.setMandatory(false); - column1HeaderRequestData.setLength(10L); - column1HeaderRequestData.setCode(""); - column1HeaderRequestData.setUnique(true); - column1HeaderRequestData.setIndexed(true); - - request.addColumnsItem(column1HeaderRequestData); - - PostColumnHeaderData column2HeaderRequestData = new PostColumnHeaderData(); - column2HeaderRequestData.setName(column2Name); - column2HeaderRequestData.setType("String"); - column2HeaderRequestData.setMandatory(false); - column2HeaderRequestData.setLength(10L); - column2HeaderRequestData.setCode(""); - column2HeaderRequestData.setUnique(false); - column2HeaderRequestData.setIndexed(true); - - request.addColumnsItem(column2HeaderRequestData); - - PostDataTablesResponse response = datatableHelper.createDatatable(request); - // Get Details of created datatable and verify unique and index - GetDataTablesResponse dataTable = datatableHelper.getDataTableDetails(datatableName); - - // verfify columns - List columnHeaderData = dataTable.getColumnHeaderData(); - assertNotNull(columnHeaderData); - - // two columns with 1 primary key and 2 audit columns created - assertEquals(columnHeaderData.size(), 5); - - // verify Only Unique is set for column with both unique and index set to true - for (ResultsetColumnHeaderData column : columnHeaderData) { - if (column.getColumnName().equalsIgnoreCase(column1Name)) { - assertTrue(column.getIsColumnUnique()); - assertFalse(column.getIsColumnIndexed()); - } - if (column.getColumnName().equalsIgnoreCase(column2Name)) { - assertTrue(column.getIsColumnIndexed()); - } - } - - } - - @Test - public void testDatableModificationWithUniqueAndIndexedColumns() { - // create dataTable - String datatableName = Utils.uniqueRandomStringGenerator("dt_" + LOAN_APP_TABLE_NAME + "_", 5).toLowerCase().toLowerCase(); - String column1Name = "itsanumber"; - String column2Name = "itsastring"; - - PostDataTablesRequest request = new PostDataTablesRequest(); - request.setDatatableName(datatableName); - request.setApptableName(LOAN_APP_TABLE_NAME); - request.setMultiRow(false); - - PostColumnHeaderData column1HeaderRequestData = new PostColumnHeaderData(); - column1HeaderRequestData.setName(column1Name); - column1HeaderRequestData.setType("Number"); - column1HeaderRequestData.setMandatory(false); - column1HeaderRequestData.setLength(10L); - column1HeaderRequestData.setCode(""); - column1HeaderRequestData.setUnique(true); - column1HeaderRequestData.setIndexed(true); - - request.addColumnsItem(column1HeaderRequestData); - - PostColumnHeaderData column2HeaderRequestData = new PostColumnHeaderData(); - column2HeaderRequestData.setName(column2Name); - column2HeaderRequestData.setType("String"); - column2HeaderRequestData.setMandatory(false); - column2HeaderRequestData.setLength(10L); - column2HeaderRequestData.setCode(""); - column2HeaderRequestData.setUnique(false); - column2HeaderRequestData.setIndexed(true); - - request.addColumnsItem(column2HeaderRequestData); - - PostDataTablesResponse response = datatableHelper.createDatatable(request); - - assertEquals(datatableName, response.getResourceIdentifier()); - - // Modify datatable add columns and change columns - PutDataTablesRequest updateRequest = new PutDataTablesRequest(); - updateRequest.setApptableName(LOAN_APP_TABLE_NAME); - - String column3Name = "number1"; - String column4Name = "number2"; - - PutDataTablesRequestAddColumns addColumn1 = new PutDataTablesRequestAddColumns(); - addColumn1.setName(column3Name); - addColumn1.setType("Number"); - addColumn1.setMandatory(false); - addColumn1.setCode(""); - addColumn1.setUnique(true); - addColumn1.setIndexed(false); - - updateRequest.addAddColumnsItem(addColumn1); - - PutDataTablesRequestAddColumns addColumn2 = new PutDataTablesRequestAddColumns(); - addColumn2.setName(column4Name); - addColumn2.setType("Number"); - addColumn2.setMandatory(false); - addColumn2.setCode(""); - addColumn2.setUnique(false); - addColumn2.setIndexed(true); - - updateRequest.addAddColumnsItem(addColumn2); - - PutDataTablesRequestChangeColumns changeColumns = new PutDataTablesRequestChangeColumns(); - changeColumns.setName(column1Name); - String newColumnName = column1Name + "new"; - changeColumns.setNewName(newColumnName); - changeColumns.setIndexed(true); - - updateRequest.addChangeColumnsItem(changeColumns); - - // update dataTable - PutDataTablesResponse updateResponse = datatableHelper.updateDatatable(datatableName, updateRequest); - - // Get Details of created datatable and verify unique and index - GetDataTablesResponse dataTable = datatableHelper.getDataTableDetails(datatableName); - - // verify columns - List columnHeaderData = dataTable.getColumnHeaderData(); - assertNotNull(columnHeaderData); - - // 2 columns with 1 primary key ,2 audit columns and 2 new columns created - assertEquals(columnHeaderData.size(), 7); - - // verify unique and index is set for new columns and renamed column has index set - for (ResultsetColumnHeaderData column : columnHeaderData) { - if (column.getColumnName().equalsIgnoreCase(column3Name)) { - assertTrue(column.getIsColumnUnique()); - } - if (column.getColumnName().equalsIgnoreCase(column4Name)) { - assertTrue(column.getIsColumnIndexed()); - } - if (column.getColumnName().equalsIgnoreCase(newColumnName)) { - assertTrue(column.getIsColumnIndexed()); - } - } - - } -} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java index c39cec3ca..29bcf04e9 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java @@ -28,6 +28,8 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import io.restassured.RestAssured; +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.builder.ResponseSpecBuilder; import io.restassured.http.ContentType; import io.restassured.path.json.JsonPath; import io.restassured.response.Response; @@ -100,6 +102,16 @@ public final class Utils { RestAssured.useRelaxedHTTPSValidation(); } + public static RequestSpecification initializeDefaultRequestSpecification() { + RequestSpecification requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); + requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); + return requestSpec; + } + + public static ResponseSpecification initializeDefaultResponseSpecification() { + return new ResponseSpecBuilder().expectStatusCode(200).build(); + } + private static void awaitSpringBootActuatorHealthyUp() { int attempt = 0; final int max_attempts = 10; diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/system/DatatableHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/system/DatatableHelper.java index c1c37be2a..526bb9469 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/system/DatatableHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/system/DatatableHelper.java @@ -18,6 +18,8 @@ */ package org.apache.fineract.integrationtests.common.system; +import static org.apache.fineract.integrationtests.common.Utils.initializeDefaultRequestSpecification; +import static org.apache.fineract.integrationtests.common.Utils.initializeDefaultResponseSpecification; import static org.junit.jupiter.api.Assertions.assertEquals; import com.google.gson.Gson; @@ -53,6 +55,10 @@ public class DatatableHelper extends IntegrationTest { private static final String DATATABLE_URL = "/fineract-provider/api/v1/datatables"; + public DatatableHelper() { + this(initializeDefaultRequestSpecification(), initializeDefaultResponseSpecification()); + } + public DatatableHelper(final RequestSpecification requestSpec, final ResponseSpecification responseSpec) { this.requestSpec = requestSpec; this.responseSpec = responseSpec; diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/datatable/DatatableEntity.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/datatable/DatatableEntity.java new file mode 100644 index 000000000..2273e00b3 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/datatable/DatatableEntity.java @@ -0,0 +1,31 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.integrationtests.datatable; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum DatatableEntity { + + LOAN("m_loan"); + + private final String referencedTableName; +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DatatableIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/datatable/DatatableIntegrationTest.java similarity index 99% rename from integration-tests/src/test/java/org/apache/fineract/integrationtests/DatatableIntegrationTest.java rename to integration-tests/src/test/java/org/apache/fineract/integrationtests/datatable/DatatableIntegrationTest.java index c33c12c72..fd17adc88 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DatatableIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/datatable/DatatableIntegrationTest.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.fineract.integrationtests; +package org.apache.fineract.integrationtests.datatable; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/datatable/DatatableTestNameGenerator.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/datatable/DatatableTestNameGenerator.java new file mode 100644 index 000000000..d7256836c --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/datatable/DatatableTestNameGenerator.java @@ -0,0 +1,30 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.integrationtests.datatable; + +import org.apache.fineract.integrationtests.common.Utils; + +public abstract class DatatableTestNameGenerator { + + private DatatableTestNameGenerator() {} + + public static String generateDatatableName(DatatableEntity datatableEntity) { + return Utils.uniqueRandomStringGenerator("dt_%s_".formatted(datatableEntity.getReferencedTableName()), 5).toLowerCase(); + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/datatable/DatatableUniqueAndIndexColumnTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/datatable/DatatableUniqueAndIndexColumnTest.java new file mode 100644 index 000000000..050910d5b --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/datatable/DatatableUniqueAndIndexColumnTest.java @@ -0,0 +1,306 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.integrationtests.datatable; + +import static org.apache.fineract.integrationtests.common.Utils.initializeRESTAssured; +import static org.apache.fineract.integrationtests.datatable.DatatableEntity.LOAN; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.client.models.GetDataTablesResponse; +import org.apache.fineract.client.models.PostColumnHeaderData; +import org.apache.fineract.client.models.PostDataTablesRequest; +import org.apache.fineract.client.models.PostDataTablesResponse; +import org.apache.fineract.client.models.PutDataTablesRequest; +import org.apache.fineract.client.models.PutDataTablesRequestAddColumns; +import org.apache.fineract.client.models.PutDataTablesRequestChangeColumns; +import org.apache.fineract.client.models.PutDataTablesResponse; +import org.apache.fineract.client.models.ResultsetColumnHeaderData; +import org.apache.fineract.integrationtests.common.system.DatatableHelper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.factory.Mappers; + +public class DatatableUniqueAndIndexColumnTest { + + private DatatableHelper datatableHelper; + + @BeforeEach + public void setup() { + initializeRESTAssured(); + this.datatableHelper = new DatatableHelper(); + } + + @Test + public void testDatableCreationWithUniqueAndIndexedColumns() { + // given + String datatableName = DatatableTestNameGenerator.generateDatatableName(LOAN); + String column1Name = "itsanumber"; + String column2Name = "itsastring"; + + PostDataTablesRequest request = new PostDataTablesRequest(); + request.setDatatableName(datatableName); + request.setApptableName(LOAN.getReferencedTableName()); + request.setMultiRow(false); + + PostColumnHeaderData column1HeaderRequestData = new PostColumnHeaderData(); + column1HeaderRequestData.setName(column1Name); + column1HeaderRequestData.setType("Number"); + column1HeaderRequestData.setMandatory(false); + column1HeaderRequestData.setLength(10L); + column1HeaderRequestData.setCode(""); + column1HeaderRequestData.setUnique(true); + column1HeaderRequestData.setIndexed(true); + + request.addColumnsItem(column1HeaderRequestData); + + PostColumnHeaderData column2HeaderRequestData = new PostColumnHeaderData(); + column2HeaderRequestData.setName(column2Name); + column2HeaderRequestData.setType("String"); + column2HeaderRequestData.setMandatory(false); + column2HeaderRequestData.setLength(10L); + column2HeaderRequestData.setCode(""); + column2HeaderRequestData.setUnique(false); + column2HeaderRequestData.setIndexed(true); + + request.addColumnsItem(column2HeaderRequestData); + + // when + PostDataTablesResponse response = datatableHelper.createDatatable(request); + + // then + assertThat(response.getResourceIdentifier()).isNotBlank(); + + GetDataTablesResponse dataTable = datatableHelper.getDataTableDetails(datatableName); + + List columnHeaderData = dataTable.getColumnHeaderData(); + assertThat(columnHeaderData).isNotNull().hasSize(5); + + List expected = List.of(new NameUniqueIndexedHeaderData(column1Name, true, true), + new NameUniqueIndexedHeaderData(column2Name, false, true)); + + NameUniqueIndexedHeaderData.Mapper mapper = Mappers.getMapper(NameUniqueIndexedHeaderData.Mapper.class); + List data = mapper.map(columnHeaderData); + + assertThat(data).containsAll(expected); + } + + @Test + public void testDatableModificationWithUniqueAndIndexedColumns() { + // given + // region Datatable creation + String datatableName = DatatableTestNameGenerator.generateDatatableName(LOAN); + String column1Name = "itsanumber"; + String column2Name = "itsastring"; + + PostDataTablesRequest request = new PostDataTablesRequest(); + request.setDatatableName(datatableName); + request.setApptableName(LOAN.getReferencedTableName()); + request.setMultiRow(false); + + PostColumnHeaderData column1HeaderRequestData = new PostColumnHeaderData(); + column1HeaderRequestData.setName(column1Name); + column1HeaderRequestData.setType("Number"); + column1HeaderRequestData.setMandatory(false); + column1HeaderRequestData.setLength(10L); + column1HeaderRequestData.setCode(""); + column1HeaderRequestData.setUnique(true); + column1HeaderRequestData.setIndexed(true); + + request.addColumnsItem(column1HeaderRequestData); + + PostColumnHeaderData column2HeaderRequestData = new PostColumnHeaderData(); + column2HeaderRequestData.setName(column2Name); + column2HeaderRequestData.setType("String"); + column2HeaderRequestData.setMandatory(false); + column2HeaderRequestData.setLength(10L); + column2HeaderRequestData.setCode(""); + column2HeaderRequestData.setUnique(false); + column2HeaderRequestData.setIndexed(true); + + request.addColumnsItem(column2HeaderRequestData); + + PostDataTablesResponse response = datatableHelper.createDatatable(request); + + assertThat(response.getResourceIdentifier()).isNotBlank(); + // endregion + + // region Datatable update + PutDataTablesRequest updateRequest = new PutDataTablesRequest(); + updateRequest.setApptableName(LOAN.getReferencedTableName()); + + String column3Name = "number1"; + String column4Name = "number2"; + + PutDataTablesRequestAddColumns addColumn1 = new PutDataTablesRequestAddColumns(); + addColumn1.setName(column3Name); + addColumn1.setType("Number"); + addColumn1.setMandatory(false); + addColumn1.setCode(""); + addColumn1.setUnique(true); + addColumn1.setIndexed(false); + + updateRequest.addAddColumnsItem(addColumn1); + + PutDataTablesRequestAddColumns addColumn2 = new PutDataTablesRequestAddColumns(); + addColumn2.setName(column4Name); + addColumn2.setType("Number"); + addColumn2.setMandatory(false); + addColumn2.setCode(""); + addColumn2.setUnique(false); + addColumn2.setIndexed(true); + + updateRequest.addAddColumnsItem(addColumn2); + + PutDataTablesRequestChangeColumns changeColumns = new PutDataTablesRequestChangeColumns(); + changeColumns.setName(column1Name); + String newColumnName = column1Name + "new"; + changeColumns.setNewName(newColumnName); + changeColumns.setIndexed(true); + + updateRequest.addChangeColumnsItem(changeColumns); + + // endregion + // when + PutDataTablesResponse updateResponse = datatableHelper.updateDatatable(datatableName, updateRequest); + + // then + GetDataTablesResponse dataTable = datatableHelper.getDataTableDetails(datatableName); + + List columnHeaderData = dataTable.getColumnHeaderData(); + assertThat(columnHeaderData).isNotNull().hasSize(7); + + List expected = List.of(new NameUniqueIndexedHeaderData(column3Name, true, true), + new NameUniqueIndexedHeaderData(column4Name, false, true), new NameUniqueIndexedHeaderData(newColumnName, false, true)); + + NameUniqueIndexedHeaderData.Mapper mapper = Mappers.getMapper(NameUniqueIndexedHeaderData.Mapper.class); + List data = mapper.map(columnHeaderData); + + assertThat(data).containsAll(expected); + } + + @Test + public void testDatableCreationMakesFkColumnIndexedIfMultirow() { + // given + String datatableName = DatatableTestNameGenerator.generateDatatableName(LOAN); + String column1Name = "itsanumber"; + + PostDataTablesRequest request = new PostDataTablesRequest(); + request.setDatatableName(datatableName); + request.setApptableName(LOAN.getReferencedTableName()); + request.setMultiRow(true); + + PostColumnHeaderData column1HeaderRequestData = new PostColumnHeaderData(); + column1HeaderRequestData.setName(column1Name); + column1HeaderRequestData.setType("Number"); + column1HeaderRequestData.setMandatory(false); + column1HeaderRequestData.setLength(10L); + column1HeaderRequestData.setCode(""); + column1HeaderRequestData.setUnique(false); + column1HeaderRequestData.setIndexed(false); + + request.addColumnsItem(column1HeaderRequestData); + + // when + PostDataTablesResponse response = datatableHelper.createDatatable(request); + + // then + assertThat(response.getResourceIdentifier()).isNotBlank(); + + GetDataTablesResponse dataTable = datatableHelper.getDataTableDetails(datatableName); + + List columnHeaderData = dataTable.getColumnHeaderData(); + assertThat(columnHeaderData).isNotNull().hasSize(5); + + List expected = List.of(new NameUniqueIndexedHeaderData("id", true, true), + new NameUniqueIndexedHeaderData("loan_id", false, true), new NameUniqueIndexedHeaderData(column1Name, false, false), + new NameUniqueIndexedHeaderData("created_at", false, false), new NameUniqueIndexedHeaderData("updated_at", false, false)); + + NameUniqueIndexedHeaderData.Mapper mapper = Mappers.getMapper(NameUniqueIndexedHeaderData.Mapper.class); + List data = mapper.map(columnHeaderData); + + assertThat(data).containsExactlyInAnyOrderElementsOf(expected); + } + + @Test + public void testDatableCreationMakesFkColumnIndexedIfNotMultirow() { + // given + String datatableName = DatatableTestNameGenerator.generateDatatableName(LOAN); + String column1Name = "itsanumber"; + + PostDataTablesRequest request = new PostDataTablesRequest(); + request.setDatatableName(datatableName); + request.setApptableName(LOAN.getReferencedTableName()); + request.setMultiRow(false); + + PostColumnHeaderData column1HeaderRequestData = new PostColumnHeaderData(); + column1HeaderRequestData.setName(column1Name); + column1HeaderRequestData.setType("Number"); + column1HeaderRequestData.setMandatory(false); + column1HeaderRequestData.setLength(10L); + column1HeaderRequestData.setCode(""); + column1HeaderRequestData.setUnique(false); + column1HeaderRequestData.setIndexed(false); + + request.addColumnsItem(column1HeaderRequestData); + + // when + PostDataTablesResponse response = datatableHelper.createDatatable(request); + + // then + assertThat(response.getResourceIdentifier()).isNotBlank(); + + GetDataTablesResponse dataTable = datatableHelper.getDataTableDetails(datatableName); + + List columnHeaderData = dataTable.getColumnHeaderData(); + assertThat(columnHeaderData).isNotNull().hasSize(4); + + List expected = List.of(new NameUniqueIndexedHeaderData("loan_id", true, true), + new NameUniqueIndexedHeaderData(column1Name, false, false), new NameUniqueIndexedHeaderData("created_at", false, false), + new NameUniqueIndexedHeaderData("updated_at", false, false)); + + NameUniqueIndexedHeaderData.Mapper mapper = Mappers.getMapper(NameUniqueIndexedHeaderData.Mapper.class); + List data = mapper.map(columnHeaderData); + + assertThat(data).containsExactlyInAnyOrderElementsOf(expected); + } + + @RequiredArgsConstructor + @Data + public static class NameUniqueIndexedHeaderData { + + private final String name; + private final boolean unique; + private final boolean indexed; + + @org.mapstruct.Mapper + public interface Mapper { + + @Mappings({ @Mapping(target = "name", source = "columnName"), @Mapping(target = "unique", source = "isColumnUnique"), + @Mapping(target = "indexed", source = "isColumnIndexed") }) + NameUniqueIndexedHeaderData map(ResultsetColumnHeaderData source); + + List map(List source); + } + } +}