FINERACT-1724: Refactored datatable index and unique constraint handling + creating an index on fk columns for multirow datatables

This commit is contained in:
Arnold Galovics
2023-03-21 11:14:35 +01:00
committed by Arnold Gálovics
parent 66c3abcefb
commit 53018a14c8
17 changed files with 515 additions and 293 deletions
@@ -233,5 +233,7 @@ dependencyManagement {
} }
dependency 'org.postgresql:postgresql:42.5.4' dependency 'org.postgresql:postgresql:42.5.4'
dependency 'org.assertj:assertj-core:3.24.2'
} }
} }
@@ -18,28 +18,24 @@
*/ */
package org.apache.fineract.infrastructure.dataqueries.handler; package org.apache.fineract.infrastructure.dataqueries.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataService; import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@Service @Service
@RequiredArgsConstructor
public class CreateDatatableCommandHandler implements NewCommandSourceHandler { public class CreateDatatableCommandHandler implements NewCommandSourceHandler {
private final ReadWriteNonCoreDataService writePlatformService; private final ReadWriteNonCoreDataService writePlatformService;
@Autowired
public CreateDatatableCommandHandler(final ReadWriteNonCoreDataService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional @Transactional
@Override @Override
public CommandProcessingResult processCommand(final JsonCommand command) { public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.createDatatable(command); return writePlatformService.createDatatable(command);
} }
} }
@@ -18,28 +18,24 @@
*/ */
package org.apache.fineract.infrastructure.dataqueries.handler; package org.apache.fineract.infrastructure.dataqueries.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataService; import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@Service @Service
@RequiredArgsConstructor
public class CreateDatatableEntryCommandHandler implements NewCommandSourceHandler { public class CreateDatatableEntryCommandHandler implements NewCommandSourceHandler {
private final ReadWriteNonCoreDataService writePlatformService; private final ReadWriteNonCoreDataService writePlatformService;
@Autowired
public CreateDatatableEntryCommandHandler(final ReadWriteNonCoreDataService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional @Transactional
@Override @Override
public CommandProcessingResult processCommand(final JsonCommand command) { public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.createNewDatatableEntry(command.entityName(), command.entityId(), command); return writePlatformService.createNewDatatableEntry(command.entityName(), command.entityId(), command);
} }
} }
@@ -18,25 +18,21 @@
*/ */
package org.apache.fineract.infrastructure.dataqueries.handler; package org.apache.fineract.infrastructure.dataqueries.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataService; import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@Service @Service
@RequiredArgsConstructor
public class UpdateDatatableCommandHandler implements NewCommandSourceHandler { public class UpdateDatatableCommandHandler implements NewCommandSourceHandler {
private final ReadWriteNonCoreDataService writePlatformService; private final ReadWriteNonCoreDataService writePlatformService;
@Autowired
public UpdateDatatableCommandHandler(final ReadWriteNonCoreDataService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional @Transactional
@Override @Override
public CommandProcessingResult processCommand(final JsonCommand command) { public CommandProcessingResult processCommand(final JsonCommand command) {
@@ -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);
}
}
@@ -33,4 +33,8 @@ public interface GenericDataService {
String wrapSQL(String sql); String wrapSQL(String sql);
List<ResultsetColumnHeaderData> fillResultsetColumnHeaders(String datatable); List<ResultsetColumnHeaderData> fillResultsetColumnHeaders(String datatable);
boolean isExplicitlyUnique(String datatable, String columnName);
boolean isExplicitlyIndexed(String datatable, String columnName);
} }
@@ -25,7 +25,7 @@ import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import javax.sql.DataSource; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.core.service.database.DatabaseIndependentQueryService; 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.ResultsetColumnValueData;
import org.apache.fineract.infrastructure.dataqueries.data.ResultsetRowData; import org.apache.fineract.infrastructure.dataqueries.data.ResultsetRowData;
import org.apache.fineract.infrastructure.dataqueries.exception.DatatableNotFoundException; import org.apache.fineract.infrastructure.dataqueries.exception.DatatableNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.rowset.SqlRowSet; import org.springframework.jdbc.support.rowset.SqlRowSet;
@@ -45,19 +44,13 @@ import org.springframework.stereotype.Service;
@Service @Service
@Slf4j @Slf4j
@RequiredArgsConstructor
public class GenericDataServiceImpl implements GenericDataService { public class GenericDataServiceImpl implements GenericDataService {
private final JdbcTemplate jdbcTemplate; private final JdbcTemplate jdbcTemplate;
private final DataSource dataSource; private final RoutingDataSource dataSource;
private final DatabaseIndependentQueryService databaseIndependentQueryService; private final DatabaseIndependentQueryService databaseIndependentQueryService;
private final DatatableKeywordGenerator datatableKeywordGenerator;
@Autowired
public GenericDataServiceImpl(final RoutingDataSource dataSource, final JdbcTemplate jdbcTemplate,
DatabaseIndependentQueryService databaseIndependentQueryService) {
this.dataSource = dataSource;
this.databaseIndependentQueryService = databaseIndependentQueryService;
this.jdbcTemplate = new JdbcTemplate(this.dataSource);
}
@Override @Override
public GenericResultsetData fillGenericResultSet(final String sql) { 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 columnNullable = "YES".equalsIgnoreCase(isNullable) || "TRUE".equalsIgnoreCase(isNullable);
final boolean columnIsPrimaryKey = "PRI".equalsIgnoreCase(isPrimaryKey) || "TRUE".equalsIgnoreCase(isPrimaryKey); final boolean columnIsPrimaryKey = "PRI".equalsIgnoreCase(isPrimaryKey) || "TRUE".equalsIgnoreCase(isPrimaryKey);
final boolean columnIsUnique = checkUnique(datatable, columnName, indexDefinitions);
boolean columnIsIndexed = false; // primary keys are automatically unique
if (!columnIsUnique) { final boolean columnIsUnique = columnIsPrimaryKey || isExplicitlyUnique(datatable, columnName, indexDefinitions);
columnIsIndexed = checkIndexed(datatable, columnName, indexDefinitions);
} // primary keys and unique constrained columns are automatically indexed
final boolean columnIsIndexed = columnIsPrimaryKey || columnIsUnique
|| isExplicitlyIndexed(datatable, columnName, indexDefinitions);
List<ResultsetColumnValueData> columnValues = new ArrayList<>(); List<ResultsetColumnValueData> columnValues = new ArrayList<>();
String codeName = null; String codeName = null;
@@ -240,7 +235,7 @@ public class GenericDataServiceImpl implements GenericDataService {
if ("varchar".equalsIgnoreCase(columnType) || "int".equalsIgnoreCase(columnType) || "integer".equalsIgnoreCase(columnType)) { if ("varchar".equalsIgnoreCase(columnType) || "int".equalsIgnoreCase(columnType) || "integer".equalsIgnoreCase(columnType)) {
if (codePosition > 0) { if (codePosition > 0) {
codeName = columnName.substring(0, codePosition); codeName = columnName.substring(0, codePosition);
columnValues = retreiveColumnValues(codeName); columnValues = retrieveColumnValues(codeName);
} }
} }
@@ -251,16 +246,26 @@ public class GenericDataServiceImpl implements GenericDataService {
return columnHeaders; return columnHeaders;
} }
private boolean checkUnique(String datatable, String columnName, List<IndexDetail> indexDefinitions) { private boolean isExplicitlyUnique(String datatable, String columnName, List<IndexDetail> indexDefinitions) {
String keyNameToCheck = "uk_" + datatable + "_" + columnName; String keyNameToCheck = datatableKeywordGenerator.generateUniqueKeyName(datatable, columnName);
return checkKeyPresent(keyNameToCheck, indexDefinitions); return checkKeyPresent(keyNameToCheck, indexDefinitions);
} }
private boolean checkIndexed(String datatable, String columnName, List<IndexDetail> indexDefinitions) { @Override
String keyNameToCheck = "idx_" + datatable + "_" + columnName; public boolean isExplicitlyUnique(String datatable, String columnName) {
return isExplicitlyUnique(datatable, columnName, getDatatableIndexData(datatable));
}
private boolean isExplicitlyIndexed(String datatable, String columnName, List<IndexDetail> indexDefinitions) {
String keyNameToCheck = datatableKeywordGenerator.generateIndexName(datatable, columnName);
return checkKeyPresent(keyNameToCheck, indexDefinitions); return checkKeyPresent(keyNameToCheck, indexDefinitions);
} }
@Override
public boolean isExplicitlyIndexed(String datatable, String columnName) {
return isExplicitlyIndexed(datatable, columnName, getDatatableIndexData(datatable));
}
private boolean checkKeyPresent(String keyNameToCheck, List<IndexDetail> indexDefinitions) { private boolean checkKeyPresent(String keyNameToCheck, List<IndexDetail> indexDefinitions) {
for (IndexDetail indexDetail : indexDefinitions) { for (IndexDetail indexDetail : indexDefinitions) {
if (indexDetail.getIndexName().equals(keyNameToCheck)) { 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 * Candidate for using caching there to get allowed 'column values' from code/codevalue tables
*/ */
private List<ResultsetColumnValueData> retreiveColumnValues(final String codeName) { private List<ResultsetColumnValueData> retrieveColumnValues(final String codeName) {
final List<ResultsetColumnValueData> columnValues = new ArrayList<>(); final List<ResultsetColumnValueData> columnValues = new ArrayList<>();
@@ -143,6 +143,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
private final ColumnValidator columnValidator; private final ColumnValidator columnValidator;
private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;
private final SqlInjectionPreventerService preventSqlInjectionService; private final SqlInjectionPreventerService preventSqlInjectionService;
private final DatatableKeywordGenerator datatableKeywordGenerator;
@Override @Override
public List<DatatableData> retrieveDatatableNames(final String appTable) { public List<DatatableData> retrieveDatatableNames(final String appTable) {
@@ -753,7 +754,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
} }
} }
if (unique) { if (unique) {
String uniqueKeyName = "uk_" + dataTableNameAlias + "_" + name; String uniqueKeyName = datatableKeywordGenerator.generateUniqueKeyName(dataTableNameAlias, name);
constrainBuilder.append(", CONSTRAINT ").append(sqlGenerator.escape(uniqueKeyName)).append(" ") constrainBuilder.append(", CONSTRAINT ").append(sqlGenerator.escape(uniqueKeyName)).append(" ")
.append("UNIQUE (" + sqlGenerator.escape(name) + ")"); .append("UNIQUE (" + sqlGenerator.escape(name) + ")");
} }
@@ -854,11 +855,19 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
if (databaseTypeResolver.isMySQL()) { if (databaseTypeResolver.isMySQL()) {
sqlBuilder.append(" ENGINE=InnoDB DEFAULT CHARSET=UTF8MB4;"); 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 // 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); createIndexesForTable(datatableName, columns);
registerDatatable(datatableName, apptableName, entitySubType); registerDatatable(datatableName, apptableName, entitySubType);
registerColumnCodeMapping(codeMappings); registerColumnCodeMapping(codeMappings);
@@ -884,6 +893,11 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withResourceIdAsString(datatableName).build(); 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) { private void createIndexesForTable(String datatableName, JsonArray columns) {
for (final JsonElement column : columns) { for (final JsonElement column : columns) {
createIndexForColumn(datatableName, column.getAsJsonObject()); createIndexForColumn(datatableName, column.getAsJsonObject());
@@ -896,8 +910,8 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
final Boolean indexed = column.has("indexed") ? column.get("indexed").getAsBoolean() : false; final Boolean indexed = column.has("indexed") ? column.get("indexed").getAsBoolean() : false;
if (indexed) { if (indexed) {
if (!unique) { if (!unique) {
String indexName = "idx_" + datatableName + "_" + name; String indexName = datatableKeywordGenerator.generateIndexName(datatableName, name);
createIndexForColumnOnTable(indexName, datatableName, name); createIndex(indexName, datatableName, name);
} }
} }
} }
@@ -1072,7 +1086,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
} }
if (unique) { if (unique) {
String uniqueKeyName = "uk_" + dataTableNameAlias + "_" + name; String uniqueKeyName = datatableKeywordGenerator.generateUniqueKeyName(dataTableNameAlias, name);
constrainBuilder.append(",ADD CONSTRAINT ").append(sqlGenerator.escape(uniqueKeyName)).append(" ") constrainBuilder.append(",ADD CONSTRAINT ").append(sqlGenerator.escape(uniqueKeyName)).append(" ")
.append("UNIQUE (" + sqlGenerator.escape(name) + ")"); .append("UNIQUE (" + sqlGenerator.escape(name) + ")");
} }
@@ -1277,7 +1291,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
sqlBuilder = sqlBuilder.deleteCharAt(indexOfFirstComma); sqlBuilder = sqlBuilder.deleteCharAt(indexOfFirstComma);
} }
sqlBuilder.append(constrainBuilder); sqlBuilder.append(constrainBuilder);
this.jdbcTemplate.execute(sqlBuilder.toString()); jdbcTemplate.execute(sqlBuilder.toString());
createIndexesForTable(datatableName, addColumns); createIndexesForTable(datatableName, addColumns);
registerColumnCodeMapping(codeMappings); registerColumnCodeMapping(codeMappings);
} }
@@ -1302,7 +1316,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
} }
sqlBuilder.append(constrainBuilder); sqlBuilder.append(constrainBuilder);
try { try {
this.jdbcTemplate.execute(sqlBuilder.toString()); jdbcTemplate.execute(sqlBuilder.toString());
deleteColumnCodeMapping(removeMappings); deleteColumnCodeMapping(removeMappings);
registerColumnCodeMapping(codeMappings); registerColumnCodeMapping(codeMappings);
// update unique constraint // update unique constraint
@@ -1310,6 +1324,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
// update indexes // update indexes
updateIndexesForTable(datatableName, changeColumns, mapColumnNameDefinition); updateIndexesForTable(datatableName, changeColumns, mapColumnNameDefinition);
} catch (final Exception e) { } catch (final Exception e) {
log.error("Exception while modifying a datatable", e);
if (e.getMessage().contains("Error on rename")) { if (e.getMessage().contains("Error on rename")) {
throw new PlatformServiceUnavailableException("error.msg.datatable.column.update.not.allowed", throw new PlatformServiceUnavailableException("error.msg.datatable.column.update.not.allowed",
"One of the column name modification not allowed", e); "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 name = column.has("name") ? column.get("name").getAsString() : null;
String columnNewName = column.has("newName") ? column.get("newName").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 setUnique = column.has("unique") ? column.get("unique").getAsBoolean() : false;
final Boolean isAlreadyUnique = columnMetaData.getIsColumnUnique(); final Boolean isAlreadyUnique = genericDataService.isExplicitlyUnique(datatableName, name);
String uniqueKeyName = "uk_" + datatableName + "_" + name; String uniqueKeyName = datatableKeywordGenerator.generateUniqueKeyName(datatableName, name);
if (isAlreadyUnique) { if (isAlreadyUnique) {
if (!setUnique) { if (!setUnique) {
@@ -1401,7 +1416,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
private void checkColumnRenameAndCreateUniqueConstraint(String datatableName, String name, String columnNewName, String constraintKey) { private void checkColumnRenameAndCreateUniqueConstraint(String datatableName, String name, String columnNewName, String constraintKey) {
if (columnNewName != null) { if (columnNewName != null) {
// create constraint with new column name // create constraint with new column name
String uniqueKeyName = "uk_" + datatableName + "_" + columnNewName; String uniqueKeyName = datatableKeywordGenerator.generateUniqueKeyName(datatableName, columnNewName);
createUniqueConstraint(datatableName, columnNewName, uniqueKeyName); createUniqueConstraint(datatableName, columnNewName, uniqueKeyName);
} else { } else {
// create constraint for column // create constraint for column
@@ -1414,7 +1429,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
// drop existing constraint // drop existing constraint
dropUniqueConstraint(datatableName, existingConstraint); dropUniqueConstraint(datatableName, existingConstraint);
// create constraint with new column name // create constraint with new column name
String uniqueKeyName = "uk_" + datatableName + "_" + columnNewName; String uniqueKeyName = datatableKeywordGenerator.generateUniqueKeyName(datatableName, columnNewName);
createUniqueConstraint(datatableName, columnNewName, uniqueKeyName); 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 setForUnique = column.has("unique") ? column.get("unique").getAsBoolean() : false;
final Boolean setForIndexed = column.has("indexed") ? column.get("indexed").getAsBoolean() : false; final Boolean setForIndexed = column.has("indexed") ? column.get("indexed").getAsBoolean() : false;
if (!setForUnique) { if (!setForUnique) {
final Boolean isAlreadyIndexed = columnMetaData.getIsColumnIndexed(); final Boolean isAlreadyIndexed = genericDataService.isExplicitlyIndexed(datatableName, name);
String uniqueIndexName = "idx_" + datatableName + "_" + name; String indexName = datatableKeywordGenerator.generateIndexName(datatableName, name);
if (isAlreadyIndexed) { if (isAlreadyIndexed) {
if (!setForIndexed) { if (!setForIndexed) {
// drop index // drop index
dropIndex(datatableName, uniqueIndexName); dropIndex(datatableName, indexName);
} else { // if column name changed } else { // if column name changed
checkColumnRenameAndModifyIndex(datatableName, columnNewName, uniqueIndexName); checkColumnRenameAndModifyIndex(datatableName, columnNewName, indexName);
} }
} else { } else {
if (setForIndexed) { 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) { private void checkColumnRenameAndCreateIndex(String datatableName, String columnExistingName, String columnNewName, String indexName) {
if (columnNewName != null) { if (columnNewName != null) {
String uniqueIndexName = "idx_" + datatableName + "_" + columnNewName; String newIndexName = datatableKeywordGenerator.generateIndexName(datatableName, columnNewName);
// create index with new column name // create index with new column name
createIndexForColumnOnTable(uniqueIndexName, datatableName, columnNewName); createIndex(newIndexName, datatableName, columnNewName);
} else { } else {
// create index with previous name // 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 // drop index with previous name
dropIndex(datatableName, existingIndex); dropIndex(datatableName, existingIndex);
// create index with new name // create index with new name
String uniqueIndexName = "idx_" + datatableName + "_" + columnNewName; String newIndexName = datatableKeywordGenerator.generateIndexName(datatableName, columnNewName);
createIndexForColumnOnTable(uniqueIndexName, datatableName, columnNewName); createIndex(newIndexName, datatableName, columnNewName);
} }
} }
private void createIndexForColumnOnTable(String uniqueIndexName, String datatableName, String columnName) { private void createIndex(String indexName, String tableName, String columnName) {
StringBuilder sqlIndexUpdateBuilder = new StringBuilder(); String safeIndexName = sqlGenerator.escape(indexName);
sqlIndexUpdateBuilder.append("CREATE INDEX ").append(sqlGenerator.escape(uniqueIndexName)).append(" ON ") String safeTableName = sqlGenerator.escape(tableName);
.append(sqlGenerator.escape(datatableName)).append(" (").append(sqlGenerator.escape(columnName)).append(");"); String safeColumnName = sqlGenerator.escape(columnName);
this.jdbcTemplate.execute(sqlIndexUpdateBuilder.toString()); String sqlIndexUpdateBuilder = "CREATE INDEX %s ON %s (%s);".formatted(safeIndexName, safeTableName, safeColumnName);
jdbcTemplate.execute(sqlIndexUpdateBuilder);
} }
private void dropIndex(String datatableName, String uniqueIndexName) { 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("ALTER TABLE ").append(sqlGenerator.escape(datatableName)).append(" ");
} }
sqlIndexUpdateBuilder.append("DROP INDEX ").append(sqlGenerator.escape(uniqueIndexName)).append(";"); sqlIndexUpdateBuilder.append("DROP INDEX ").append(sqlGenerator.escape(uniqueIndexName)).append(";");
this.jdbcTemplate.execute(sqlIndexUpdateBuilder.toString()); jdbcTemplate.execute(sqlIndexUpdateBuilder.toString());
} }
@Transactional @Transactional
@@ -1662,6 +1678,7 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
} }
@Override @Override
@Transactional(readOnly = true)
public GenericResultsetData retrieveDataTableGenericResultSet(final String dataTableName, final Long appTableId, final String order, public GenericResultsetData retrieveDataTableGenericResultSet(final String dataTableName, final Long appTableId, final String order,
final Long id) { final Long id) {
@@ -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.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.core.service.database.DatabaseTypeResolver; import org.apache.fineract.infrastructure.core.service.database.DatabaseTypeResolver;
import org.apache.fineract.infrastructure.dataqueries.data.DataTableValidator; 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.GenericDataService;
import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataService; import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataService;
import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataServiceImpl; import org.apache.fineract.infrastructure.dataqueries.service.ReadWriteNonCoreDataServiceImpl;
@@ -48,9 +49,10 @@ public class DataQueriesAutoConfiguration {
final DatatableCommandFromApiJsonDeserializer fromApiJsonDeserializer, final DatatableCommandFromApiJsonDeserializer fromApiJsonDeserializer,
final ConfigurationDomainService configurationDomainService, final CodeReadPlatformService codeReadPlatformService, final ConfigurationDomainService configurationDomainService, final CodeReadPlatformService codeReadPlatformService,
final DataTableValidator dataTableValidator, final ColumnValidator columnValidator, 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, return new ReadWriteNonCoreDataServiceImpl(jdbcTemplate, databaseTypeResolver, sqlGenerator, context, fromJsonHelper,
genericDataService, fromApiJsonDeserializer, configurationDomainService, codeReadPlatformService, dataTableValidator, genericDataService, fromApiJsonDeserializer, configurationDomainService, codeReadPlatformService, dataTableValidator,
columnValidator, namedParameterJdbcTemplate, preventSqlInjectionService); columnValidator, namedParameterJdbcTemplate, preventSqlInjectionService, datatableKeywordGenerator);
} }
} }
+6
View File
@@ -43,5 +43,11 @@ dependencies {
testCompileOnly 'org.projectlombok:lombok' testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor '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' testImplementation 'com.github.tomakehurst:wiremock-jre8:2.35.0'
} }
@@ -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<ResultsetColumnHeaderData> 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<ResultsetColumnHeaderData> 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());
}
}
}
}
@@ -28,6 +28,8 @@ import com.google.gson.JsonArray;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import io.restassured.RestAssured; import io.restassured.RestAssured;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.builder.ResponseSpecBuilder;
import io.restassured.http.ContentType; import io.restassured.http.ContentType;
import io.restassured.path.json.JsonPath; import io.restassured.path.json.JsonPath;
import io.restassured.response.Response; import io.restassured.response.Response;
@@ -100,6 +102,16 @@ public final class Utils {
RestAssured.useRelaxedHTTPSValidation(); 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() { private static void awaitSpringBootActuatorHealthyUp() {
int attempt = 0; int attempt = 0;
final int max_attempts = 10; final int max_attempts = 10;
@@ -18,6 +18,8 @@
*/ */
package org.apache.fineract.integrationtests.common.system; 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 static org.junit.jupiter.api.Assertions.assertEquals;
import com.google.gson.Gson; 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"; private static final String DATATABLE_URL = "/fineract-provider/api/v1/datatables";
public DatatableHelper() {
this(initializeDefaultRequestSpecification(), initializeDefaultResponseSpecification());
}
public DatatableHelper(final RequestSpecification requestSpec, final ResponseSpecification responseSpec) { public DatatableHelper(final RequestSpecification requestSpec, final ResponseSpecification responseSpec) {
this.requestSpec = requestSpec; this.requestSpec = requestSpec;
this.responseSpec = responseSpec; this.responseSpec = responseSpec;
@@ -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;
}
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * 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.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -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();
}
}
@@ -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<ResultsetColumnHeaderData> columnHeaderData = dataTable.getColumnHeaderData();
assertThat(columnHeaderData).isNotNull().hasSize(5);
List<NameUniqueIndexedHeaderData> expected = List.of(new NameUniqueIndexedHeaderData(column1Name, true, true),
new NameUniqueIndexedHeaderData(column2Name, false, true));
NameUniqueIndexedHeaderData.Mapper mapper = Mappers.getMapper(NameUniqueIndexedHeaderData.Mapper.class);
List<NameUniqueIndexedHeaderData> 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<ResultsetColumnHeaderData> columnHeaderData = dataTable.getColumnHeaderData();
assertThat(columnHeaderData).isNotNull().hasSize(7);
List<NameUniqueIndexedHeaderData> 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<NameUniqueIndexedHeaderData> 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<ResultsetColumnHeaderData> columnHeaderData = dataTable.getColumnHeaderData();
assertThat(columnHeaderData).isNotNull().hasSize(5);
List<NameUniqueIndexedHeaderData> 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<NameUniqueIndexedHeaderData> 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<ResultsetColumnHeaderData> columnHeaderData = dataTable.getColumnHeaderData();
assertThat(columnHeaderData).isNotNull().hasSize(4);
List<NameUniqueIndexedHeaderData> 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<NameUniqueIndexedHeaderData> 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<NameUniqueIndexedHeaderData> map(List<ResultsetColumnHeaderData> source);
}
}
}