Skip to content

Commit

Permalink
Merge branch 'master' into DHIS2-18417
Browse files Browse the repository at this point in the history
  • Loading branch information
larshelge committed Dec 16, 2024
2 parents 791ec72 + 191d988 commit d376053
Show file tree
Hide file tree
Showing 17 changed files with 459 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,6 @@ public enum DimensionItemType {
ORGANISATION_UNIT_GROUP,
CATEGORY_OPTION_GROUP,
EXPRESSION_DIMENSION_ITEM,
SUBEXPRESSION_DIMENSION_ITEM
SUBEXPRESSION_DIMENSION_ITEM,
OPTION_SET
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ private static List<AnalyticsTableColumn> getCommonColumns(SqlBuilder sqlBuilder
AnalyticsTableColumn.builder()
.name(EnrollmentAnalyticsColumnName.COMPLETED_DATE_COLUMN_NAME)
.dataType(TIMESTAMP)
.selectExpression("case en.status when 'COMPLETED' then en.completeddate end")
.selectExpression(sqlBuilder.ifThen("en.status = 'COMPLETED'", "en.completeddate"))
.build(),
AnalyticsTableColumn.builder()
.name(EnrollmentAnalyticsColumnName.LAST_UPDATED_COLUMN_NAME)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,18 @@ private static List<AnalyticsTableColumn> getCommonColumns(SqlBuilder sqlBuilder
AnalyticsTableColumn.builder()
.name(EventAnalyticsColumnName.CREATED_COLUMN_NAME)
.dataType(TIMESTAMP)
.selectExpression(firstIfNotNullOrElse("ev.createdatclient", "ev.created"))
.selectExpression(
sqlBuilder.ifThenElse(
"ev.createdatclient is not null", "ev.createdatclient", "ev.created"))
.build(),
AnalyticsTableColumn.builder()
.name(EventAnalyticsColumnName.LAST_UPDATED_COLUMN_NAME)
.dataType(TIMESTAMP)
.selectExpression(firstIfNotNullOrElse("ev.lastupdatedatclient", "ev.lastupdated"))
.selectExpression(
sqlBuilder.ifThenElse(
"ev.lastupdatedatclient is not null",
"ev.lastupdatedatclient",
"ev.lastupdated"))
.build(),
AnalyticsTableColumn.builder()
.name(EventAnalyticsColumnName.STORED_BY_COLUMN_NAME)
Expand Down Expand Up @@ -306,16 +312,4 @@ private static List<AnalyticsTableColumn> getJsonColumns(SqlBuilder sqlBuilder)
.skipIndex(Skip.SKIP)
.build());
}

/**
* Returns a SQL expression that returns the first argument if it is not null, otherwise the
* second argument.
*
* @param first the first argument
* @param second the second argument
* @return a SQL expression
*/
private static String firstIfNotNullOrElse(String first, String second) {
return "case when " + first + " is not null then " + first + " else " + second + " end";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
/*
* Copyright (c) 2004-2022, University of Oslo
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* Neither the name of the HISP project nor the names of its contributors may
* be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.hisp.dhis.dataitem.query;

import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.hisp.dhis.dataitem.query.shared.FilteringStatement.always;
import static org.hisp.dhis.dataitem.query.shared.FilteringStatement.displayNameFiltering;
import static org.hisp.dhis.dataitem.query.shared.FilteringStatement.displayShortNameFiltering;
import static org.hisp.dhis.dataitem.query.shared.FilteringStatement.identifiableTokenFiltering;
import static org.hisp.dhis.dataitem.query.shared.FilteringStatement.ifAny;
import static org.hisp.dhis.dataitem.query.shared.FilteringStatement.ifSet;
import static org.hisp.dhis.dataitem.query.shared.FilteringStatement.nameFiltering;
import static org.hisp.dhis.dataitem.query.shared.FilteringStatement.rootJunction;
import static org.hisp.dhis.dataitem.query.shared.FilteringStatement.shortNameFiltering;
import static org.hisp.dhis.dataitem.query.shared.FilteringStatement.uidFiltering;
import static org.hisp.dhis.dataitem.query.shared.LimitStatement.maxLimit;
import static org.hisp.dhis.dataitem.query.shared.NameTranslationStatement.translationNamesColumnsFor;
import static org.hisp.dhis.dataitem.query.shared.NameTranslationStatement.translationNamesJoinsOn;
import static org.hisp.dhis.dataitem.query.shared.OrderingStatement.ordering;
import static org.hisp.dhis.dataitem.query.shared.ParamPresenceChecker.hasNonBlankStringPresence;
import static org.hisp.dhis.dataitem.query.shared.QueryParam.LOCALE;
import static org.hisp.dhis.dataitem.query.shared.StatementUtil.SPACED_FROM;
import static org.hisp.dhis.dataitem.query.shared.StatementUtil.SPACED_LEFT_PARENTHESIS;
import static org.hisp.dhis.dataitem.query.shared.StatementUtil.SPACED_RIGHT_PARENTHESIS;
import static org.hisp.dhis.dataitem.query.shared.StatementUtil.SPACED_SELECT;
import static org.hisp.dhis.dataitem.query.shared.StatementUtil.SPACED_WHERE;
import static org.hisp.dhis.dataitem.query.shared.UserAccessStatement.checkOwnerConditions;

import java.util.List;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair;
import org.hisp.dhis.common.BaseIdentifiableObject;
import org.hisp.dhis.dataitem.query.shared.OptionalFilterBuilder;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.stereotype.Component;

/**
* This component is responsible for providing query capabilities on top of ExpressionDimensionItem
* objects.
*/
@Slf4j
@Component
public class OptionSetQuery implements DataItemQuery {
public static final String CAST_NULL_AS_TEXT = "cast (null as text)";

private static final String COMMON_COLUMNS =
List.of(
Pair.of("program_name", CAST_NULL_AS_TEXT),
Pair.of("program_uid", CAST_NULL_AS_TEXT),
Pair.of("program_shortname", CAST_NULL_AS_TEXT),
Pair.of("item_uid", "optionset.uid"),
Pair.of("item_name", "optionset.name"),
Pair.of("item_shortname", CAST_NULL_AS_TEXT),
Pair.of("item_valuetype", CAST_NULL_AS_TEXT),
Pair.of("item_code", "optionset.code"),
Pair.of("item_sharing", "optionset.sharing"),
Pair.of("item_domaintype", CAST_NULL_AS_TEXT),
Pair.of("item_type", "cast ('OPTION_SET' as text)"),
Pair.of("expression", CAST_NULL_AS_TEXT))
.stream()
.map(pair -> pair.getRight() + " as " + pair.getLeft())
.collect(Collectors.joining(", "));

/**
* Builds and returns the SQL statement required by the implementation.
*
* @param paramsMap
* @return the full SQL statement
*/
@Override
public String getStatement(MapSqlParameterSource paramsMap) {
StringBuilder sql = new StringBuilder();

sql.append(SPACED_LEFT_PARENTHESIS);

// Creating a temp translated table to be queried.
sql.append(SPACED_SELECT + "*" + SPACED_FROM + SPACED_LEFT_PARENTHESIS);

if (hasNonBlankStringPresence(paramsMap, LOCALE)) {
// Selecting translated names.
sql.append(selectRowsContainingTranslatedName());
} else {
// Retrieving all rows ignoring translation as no locale is defined.
sql.append(selectAllRowsIgnoringAnyTranslation());
}

sql.append(
" group by item_name, item_uid, item_code, item_sharing, item_shortname,"
+ " i18n_first_name, i18n_first_shortname, i18n_second_name, i18n_second_shortname, expression");

// Closing the temp table.
sql.append(SPACED_RIGHT_PARENTHESIS + " t");

sql.append(SPACED_WHERE);

// Applying filters, ordering and limits.

// Mandatory filters. They do not respect the root junction filtering.
sql.append(always(checkOwnerConditions("t.item_sharing")));

// Optional filters, based on the current root junction.
OptionalFilterBuilder optionalFilters = new OptionalFilterBuilder(paramsMap);
optionalFilters.append(ifSet(displayNameFiltering("t.i18n_first_name", paramsMap)));
optionalFilters.append(ifSet(displayShortNameFiltering("t.i18n_first_shortname", paramsMap)));
optionalFilters.append(ifSet(nameFiltering("t.item_name", paramsMap)));
optionalFilters.append(ifSet(shortNameFiltering("t.item_shortname", paramsMap)));
optionalFilters.append(ifSet(uidFiltering("t.item_uid", paramsMap)));
sql.append(ifAny(optionalFilters.toString()));

String identifiableStatement =
identifiableTokenFiltering(
"t.item_uid", "t.item_code", "t.i18n_first_name", null, paramsMap);

if (isNotBlank(identifiableStatement)) {
sql.append(rootJunction(paramsMap));
sql.append(identifiableStatement);
}

sql.append(
ifSet(
ordering(
"t.i18n_first_name, t.i18n_second_name, t.item_uid",
"t.item_name, t.item_uid",
"t.i18n_first_shortname, t.i18n_second_shortname, t.item_uid",
"t.item_shortname, t.item_uid",
paramsMap)));
sql.append(ifSet(maxLimit(paramsMap)));
sql.append(SPACED_RIGHT_PARENTHESIS);

String fullStatement = sql.toString();

log.trace("Full SQL: " + fullStatement);

return fullStatement;
}

/**
* Checks if the query rules match the required conditions so the query can be executed. This
* implementation must return always true.
*
* @param paramsMap
* @return true if matches, false otherwise
*/
@Override
public boolean matchQueryRules(MapSqlParameterSource paramsMap) {
return true;
}

/**
* Simply returns the entity associated with the respective interface/query implementation.
*
* @return the entity associated to the interface implementation
*/
@Override
public Class<? extends BaseIdentifiableObject> getRootEntity() {
return QueryableDataItem.OPTION_SET.getEntity();
}

private String selectRowsContainingTranslatedName() {
StringBuilder sql = new StringBuilder();

sql.append(SPACED_SELECT)
.append(COMMON_COLUMNS)
.append(translationNamesColumnsFor("optionset", false, false));

sql.append(" from optionset ").append(translationNamesJoinsOn("optionset"));

return sql.toString();
}

private String selectAllRowsIgnoringAnyTranslation() {
return new StringBuilder()
.append(SPACED_SELECT + COMMON_COLUMNS)
.append(", optionset.name as i18n_first_name, cast (null as text) as i18n_second_name")
.append(
", "
+ CAST_NULL_AS_TEXT
+ " as i18n_first_shortname, cast (null as text) as i18n_second_shortname")
.append(" from optionset ")
.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import org.hisp.dhis.dataset.DataSet;
import org.hisp.dhis.expressiondimensionitem.ExpressionDimensionItem;
import org.hisp.dhis.indicator.Indicator;
import org.hisp.dhis.option.OptionSet;
import org.hisp.dhis.program.ProgramDataElementDimensionItem;
import org.hisp.dhis.program.ProgramIndicator;
import org.hisp.dhis.program.ProgramTrackedEntityAttributeDimensionItem;
Expand All @@ -52,7 +53,8 @@ public enum QueryableDataItem {
PROGRAM_INDICATOR(ProgramIndicator.class),
PROGRAM_DATA_ELEMENT(ProgramDataElementDimensionItem.class),
PROGRAM_ATTRIBUTE(ProgramTrackedEntityAttributeDimensionItem.class),
EXPRESSION_DIMENSION_ITEM(ExpressionDimensionItem.class);
EXPRESSION_DIMENSION_ITEM(ExpressionDimensionItem.class),
OPTION_SET(OptionSet.class);

private Class<? extends BaseIdentifiableObject> entity;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,20 @@ public static String translationNamesColumnsFor(String table) {
* @return the columns containing the translated names
*/
public static String translationNamesColumnsFor(String table, boolean includeProgram) {
return translationNamesColumnsFor(table, includeProgram, true);
}

/**
* This method defines the values for the translatable columns, for the given table. Depending on
* the program flag it will also bring translatable columns from the program table.
*
* @param table the table containing the translation columns
* @param includeProgram if true, it will also bring program columns
* @param hasShortName whether the table has a shortname column
* @return the columns containing the translated names
*/
public static String translationNamesColumnsFor(
String table, boolean includeProgram, boolean hasShortName) {
StringBuilder columns = new StringBuilder();

if (isNotBlank(table)) {
Expand All @@ -141,10 +155,10 @@ public static String translationNamesColumnsFor(String table, boolean includePro
", (case when p_displayname.value is not null then p_displayname.value else program.name end) as i18n_first_name")
.append(
", (case when p_displayshortname.value is not null then p_displayshortname.value else program.shortname end) as i18n_first_shortname")
.append(translationNamesColumnsForItem(table, "i18n_second"));
.append(translationNamesColumnsForItem(table, "i18n_second", hasShortName));
} else {
columns
.append(translationNamesColumnsForItem(table, "i18n_first"))
.append(translationNamesColumnsForItem(table, "i18n_first", hasShortName))
.append(", cast (null as text) as i18n_second_name")
.append(", cast (null as text) as i18n_second_shortname");
}
Expand All @@ -153,7 +167,8 @@ public static String translationNamesColumnsFor(String table, boolean includePro
return columns.toString();
}

private static String translationNamesColumnsForItem(String table, String i18nColumnPrefix) {
private static String translationNamesColumnsForItem(
String table, String i18nColumnPrefix, boolean hasShortName) {
StringBuilder columns = new StringBuilder();

columns
Expand All @@ -173,8 +188,8 @@ private static String translationNamesColumnsForItem(String table, String i18nCo
+ "_displayshortname.value is not null then "
+ table
+ "_displayshortname.value else "
+ table
+ ".shortname end) as "
+ (hasShortName ? table + ".shortname" : "cast (null as text)")
+ " end) as "
+ i18nColumnPrefix
+ "_shortname");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@ public String singleQuotedCommaDelimited(Collection<String> items) {
: items.stream().map(this::singleQuote).collect(Collectors.joining(COMMA));
}

@Override
public String concat(String... columns) {
return "concat(" + String.join(", ", columns) + ")";
}

@Override
public String trim(String expression) {
return "trim(" + expression + ")";
}

// Index types

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,16 +213,6 @@ public String regexpMatch(String value, String pattern) {
return String.format("match(%s, %s)", value, pattern);
}

@Override
public String concat(String... columns) {
return "concat(" + String.join(", ", columns) + ")";
}

@Override
public String trim(String expression) {
return "trim(" + expression + ")";
}

@Override
public String coalesce(String expression, String defaultValue) {
return "coalesce(" + expression + ", " + defaultValue + ")";
Expand Down Expand Up @@ -259,6 +249,16 @@ public String dateDifference(String startDate, String endDate, DateUnit dateUnit
throw new UnsupportedOperationException();
}

@Override
public String ifThen(String condition, String result) {
return String.format("if(%s, %s, null)", condition, result);
}

@Override
public String ifThenElse(String condition, String resultA, String resultB) {
return String.format("if(%s, %s, %s)", condition, resultA, resultB);
}

// Statements

@Override
Expand Down
Loading

0 comments on commit d376053

Please sign in to comment.