From 5c7bd2791aa765ed1ebb2592048c2f96eb77669c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20Helge=20=C3=98verland?= Date: Mon, 16 Dec 2024 10:21:37 +0100 Subject: [PATCH 1/5] fix: Use SqlBuilder for if-then-else logic [DHIS2-18417] (#19472) --- .../table/EnrollmentAnalyticsColumn.java | 2 +- .../analytics/table/EventAnalyticsColumn.java | 22 +++++-------- .../hisp/dhis/db/sql/AbstractSqlBuilder.java | 10 ++++++ .../dhis/db/sql/ClickHouseSqlBuilder.java | 20 ++++++------ .../org/hisp/dhis/db/sql/DorisSqlBuilder.java | 20 ++++++------ .../hisp/dhis/db/sql/PostgreSqlBuilder.java | 31 +++++++++---------- .../java/org/hisp/dhis/db/sql/SqlBuilder.java | 19 ++++++++++++ .../dhis/db/sql/ClickHouseSqlBuilderTest.java | 24 ++++++++++++++ .../hisp/dhis/db/sql/DorisSqlBuilderTest.java | 26 +++++++++++++++- .../dhis/db/sql/PostgreSqlBuilderTest.java | 24 ++++++++++++++ 10 files changed, 146 insertions(+), 52 deletions(-) diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/EnrollmentAnalyticsColumn.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/EnrollmentAnalyticsColumn.java index ff2f4dc1270c..1ab66fc0fe3e 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/EnrollmentAnalyticsColumn.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/EnrollmentAnalyticsColumn.java @@ -107,7 +107,7 @@ private static List 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) diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/EventAnalyticsColumn.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/EventAnalyticsColumn.java index cd65f8081200..5f3995d048ab 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/EventAnalyticsColumn.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/EventAnalyticsColumn.java @@ -140,12 +140,18 @@ private static List 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) @@ -306,16 +312,4 @@ private static List 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"; - } } diff --git a/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/AbstractSqlBuilder.java b/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/AbstractSqlBuilder.java index 6e39516d818e..049f0f27a025 100644 --- a/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/AbstractSqlBuilder.java +++ b/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/AbstractSqlBuilder.java @@ -81,6 +81,16 @@ public String singleQuotedCommaDelimited(Collection 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 diff --git a/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/ClickHouseSqlBuilder.java b/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/ClickHouseSqlBuilder.java index 6381be607075..a42cab502fcc 100644 --- a/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/ClickHouseSqlBuilder.java +++ b/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/ClickHouseSqlBuilder.java @@ -208,16 +208,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 + ")"; @@ -254,6 +244,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 diff --git a/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/DorisSqlBuilder.java b/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/DorisSqlBuilder.java index 48bfe6bc31b8..7a7350f70f87 100644 --- a/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/DorisSqlBuilder.java +++ b/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/DorisSqlBuilder.java @@ -211,16 +211,6 @@ public String regexpMatch(String value, String pattern) { return String.format("%s regexp %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 + ")"; @@ -263,6 +253,16 @@ public String dateDifference(String startDate, String endDate, DateUnit dateUnit }; } + @Override + public String ifThen(String condition, String result) { + return String.format("case when %s then %s end", condition, result); + } + + @Override + public String ifThenElse(String condition, String resultA, String resultB) { + return String.format("case when %s then %s else %s end", condition, resultA, resultB); + } + // Statements @Override diff --git a/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/PostgreSqlBuilder.java b/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/PostgreSqlBuilder.java index df43166bd281..55c100f1235e 100644 --- a/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/PostgreSqlBuilder.java +++ b/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/PostgreSqlBuilder.java @@ -230,16 +230,6 @@ public String regexpMatch(String value, String pattern) { return String.format("%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 + ")"; @@ -257,12 +247,11 @@ public String jsonExtractNested(String column, String... expression) { @Override public String cast(String column, DataType dataType) { - return column - + switch (dataType) { - case NUMERIC -> "::numeric"; - case BOOLEAN -> "::numeric!=0"; - case TEXT -> "::text"; - }; + return switch (dataType) { + case NUMERIC -> String.format("%s::numeric", column); + case BOOLEAN -> String.format("%s::numeric!=0", column); + case TEXT -> String.format("%s::text", column); + }; } @Override @@ -291,6 +280,16 @@ public String dateDifference(String startDate, String endDate, DateUnit dateUnit }; } + @Override + public String ifThen(String condition, String result) { + return String.format("case when %s then %s end", condition, result); + } + + @Override + public String ifThenElse(String condition, String resultA, String resultB) { + return String.format("case when %s then %s else %s end", condition, resultA, resultB); + } + // Statements @Override diff --git a/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/SqlBuilder.java b/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/SqlBuilder.java index 15f167775fc4..82e526db9a0d 100644 --- a/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/SqlBuilder.java +++ b/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/SqlBuilder.java @@ -324,6 +324,25 @@ public interface SqlBuilder { */ String dateDifference(String startDate, String endDate, DateUnit dateUnit); + /** + * Returns a conditional statement. + * + * @param condition the condition to evaluate. + * @param result the result to return if the condition is true. + * @return a conditional statement. + */ + String ifThen(String condition, String result); + + /** + * Returns a conditional statement. + * + * @param condition the condition to evaluate. + * @param resultA the result to return if the condition is true. + * @param resultB the result to return if the condition is false. + * @return a conditional statement. + */ + String ifThenElse(String condition, String resultA, String resultB); + // Statements /** diff --git a/dhis-2/dhis-support/dhis-support-sql/src/test/java/org/hisp/dhis/db/sql/ClickHouseSqlBuilderTest.java b/dhis-2/dhis-support/dhis-support-sql/src/test/java/org/hisp/dhis/db/sql/ClickHouseSqlBuilderTest.java index b5cb15bd1e03..9e26f64114a3 100644 --- a/dhis-2/dhis-support/dhis-support-sql/src/test/java/org/hisp/dhis/db/sql/ClickHouseSqlBuilderTest.java +++ b/dhis-2/dhis-support/dhis-support-sql/src/test/java/org/hisp/dhis/db/sql/ClickHouseSqlBuilderTest.java @@ -170,6 +170,16 @@ void testEscape() { assertEquals("Level ''high'' found", sqlBuilder.escape("Level 'high' found")); } + @Test + void testConcat() { + assertEquals("concat(de.uid, pe.iso, ou.uid)", sqlBuilder.concat("de.uid", "pe.iso", "ou.uid")); + } + + @Test + void testTrim() { + assertEquals("trim(ax.value)", sqlBuilder.trim("ax.value")); + } + @Test void testSinqleQuotedCommaDelimited() { assertEquals( @@ -227,6 +237,20 @@ void testJsonExtractNested() { sqlBuilder.jsonExtractNested("eventdatavalues", "D7m8vpzxHDJ", "value")); } + @Test + void testIfThen() { + assertEquals( + "if(a.status = 'COMPLETE', a.eventdate, null)", + sqlBuilder.ifThen("a.status = 'COMPLETE'", "a.eventdate")); + } + + @Test + void testIfThenElse() { + assertEquals( + "if(a.status = 'COMPLETE', a.eventdate, a.scheduleddate)", + sqlBuilder.ifThenElse("a.status = 'COMPLETE'", "a.eventdate", "a.scheduleddate")); + } + // Statements @Test diff --git a/dhis-2/dhis-support/dhis-support-sql/src/test/java/org/hisp/dhis/db/sql/DorisSqlBuilderTest.java b/dhis-2/dhis-support/dhis-support-sql/src/test/java/org/hisp/dhis/db/sql/DorisSqlBuilderTest.java index eeebfee4ce5d..d914ebdf1346 100644 --- a/dhis-2/dhis-support/dhis-support-sql/src/test/java/org/hisp/dhis/db/sql/DorisSqlBuilderTest.java +++ b/dhis-2/dhis-support/dhis-support-sql/src/test/java/org/hisp/dhis/db/sql/DorisSqlBuilderTest.java @@ -154,6 +154,16 @@ void testEscape() { assertEquals("C:\\\\Downloads\\\\File.doc", sqlBuilder.escape("C:\\Downloads\\File.doc")); } + @Test + void testConcat() { + assertEquals("concat(de.uid, pe.iso, ou.uid)", sqlBuilder.concat("de.uid", "pe.iso", "ou.uid")); + } + + @Test + void testTrim() { + assertEquals("trim(ax.value)", sqlBuilder.trim("ax.value")); + } + @Test void testSinqleQuotedCommaDelimited() { assertEquals( @@ -211,6 +221,20 @@ void testJsonExtractNested() { sqlBuilder.jsonExtractNested("eventdatavalues", "D7m8vpzxHDJ", "value")); } + @Test + void testIfThen() { + assertEquals( + "case when a.status = 'COMPLETE' then a.eventdate end", + sqlBuilder.ifThen("a.status = 'COMPLETE'", "a.eventdate")); + } + + @Test + void testIfThenElse() { + assertEquals( + "case when a.status = 'COMPLETE' then a.eventdate else a.scheduleddate end", + sqlBuilder.ifThenElse("a.status = 'COMPLETE'", "a.eventdate", "a.scheduleddate")); + } + // Statements @Test @@ -246,7 +270,7 @@ distributed by hash(`id`) buckets 10 \ assertEquals(expected, sqlBuilder.createTable(table)); } - // void testCreateTableB() + // TO DO void testCreateTableB() @Test void testCreateTableC() { diff --git a/dhis-2/dhis-support/dhis-support-sql/src/test/java/org/hisp/dhis/db/sql/PostgreSqlBuilderTest.java b/dhis-2/dhis-support/dhis-support-sql/src/test/java/org/hisp/dhis/db/sql/PostgreSqlBuilderTest.java index c2c61563b0aa..935336050c2b 100644 --- a/dhis-2/dhis-support/dhis-support-sql/src/test/java/org/hisp/dhis/db/sql/PostgreSqlBuilderTest.java +++ b/dhis-2/dhis-support/dhis-support-sql/src/test/java/org/hisp/dhis/db/sql/PostgreSqlBuilderTest.java @@ -194,6 +194,16 @@ void testSinqleQuotedCommaDelimited() { assertEquals("", sqlBuilder.singleQuotedCommaDelimited(null)); } + @Test + void testConcat() { + assertEquals("concat(de.uid, pe.iso, ou.uid)", sqlBuilder.concat("de.uid", "pe.iso", "ou.uid")); + } + + @Test + void testTrim() { + assertEquals("trim(ax.value)", sqlBuilder.trim("ax.value")); + } + @Test void testQualifyTable() { assertEquals("\"category\"", sqlBuilder.qualifyTable("category")); @@ -237,6 +247,20 @@ void testJsonExtractNested() { sqlBuilder.jsonExtractNested("eventdatavalues", "D7m8vpzxHDJ", "value")); } + @Test + void testIfThen() { + assertEquals( + "case when a.status = 'COMPLETE' then a.eventdate end", + sqlBuilder.ifThen("a.status = 'COMPLETE'", "a.eventdate")); + } + + @Test + void testIfThenElse() { + assertEquals( + "case when a.status = 'COMPLETE' then a.eventdate else a.scheduleddate end", + sqlBuilder.ifThenElse("a.status = 'COMPLETE'", "a.eventdate", "a.scheduleddate")); + } + // Statements @Test From 1a25f607594cf6905f59caef7ab86daeaf897c42 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:47:04 +0100 Subject: [PATCH 2/5] chore(deps): bump org.apache.commons:commons-text in /dhis-2 (#19477) Bumps org.apache.commons:commons-text from 1.12.0 to 1.13.0. --- updated-dependencies: - dependency-name: org.apache.commons:commons-text dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dhis-2/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dhis-2/pom.xml b/dhis-2/pom.xml index 9c268f172b3b..5e6adc39d262 100644 --- a/dhis-2/pom.xml +++ b/dhis-2/pom.xml @@ -192,7 +192,7 @@ 4.4 1.9.4 3.17.0 - 1.12.0 + 1.13.0 1.5 3.6.1 1.9.0 From 7e30b38ad1faad496aba2768e915677b674c2032 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 08:08:35 -0300 Subject: [PATCH 3/5] chore(deps): bump log4j.version in /dhis-2/dhis-test-e2e (#19473) Bumps `log4j.version` from 2.24.2 to 2.24.3. Updates `org.apache.logging.log4j:log4j-api` from 2.24.2 to 2.24.3 Updates `org.apache.logging.log4j:log4j-core` from 2.24.2 to 2.24.3 --- updated-dependencies: - dependency-name: org.apache.logging.log4j:log4j-api dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.logging.log4j:log4j-core dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dhis-2/dhis-test-e2e/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dhis-2/dhis-test-e2e/pom.xml b/dhis-2/dhis-test-e2e/pom.xml index a637a7e42a13..6029aa1ae56a 100644 --- a/dhis-2/dhis-test-e2e/pom.xml +++ b/dhis-2/dhis-test-e2e/pom.xml @@ -15,7 +15,7 @@ 1.4.0 5.11.3 2.11.0 - 2.24.2 + 2.24.3 5.5.0 2.18.2 33.3.1-jre From d42443e67021a8b9a99477396c12fa3e131f4eda Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 08:08:54 -0300 Subject: [PATCH 4/5] chore(deps): bump log4j.version from 2.24.2 to 2.24.3 in /dhis-2 (#19476) Bumps `log4j.version` from 2.24.2 to 2.24.3. Updates `org.apache.logging.log4j:log4j-api` from 2.24.2 to 2.24.3 Updates `org.apache.logging.log4j:log4j-core` from 2.24.2 to 2.24.3 Updates `org.apache.logging.log4j:log4j-layout-template-json` from 2.24.2 to 2.24.3 Updates `org.apache.logging.log4j:log4j-web` from 2.24.2 to 2.24.3 Updates `org.apache.logging.log4j:log4j-slf4j-impl` from 2.24.2 to 2.24.3 --- updated-dependencies: - dependency-name: org.apache.logging.log4j:log4j-api dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.logging.log4j:log4j-core dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.logging.log4j:log4j-layout-template-json dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.logging.log4j:log4j-web dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.logging.log4j:log4j-slf4j-impl dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dhis-2/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dhis-2/pom.xml b/dhis-2/pom.xml index 5e6adc39d262..489db5b714b8 100644 --- a/dhis-2/pom.xml +++ b/dhis-2/pom.xml @@ -209,7 +209,7 @@ 1.3.1 - 2.24.2 + 2.24.3 1.7.36 From 191d988dcdfe32e910720c173e0ea36dc2f50b98 Mon Sep 17 00:00:00 2001 From: Giuseppe Nespolino Date: Mon, 16 Dec 2024 12:12:31 +0100 Subject: [PATCH 5/5] feat: add optionset to dataItems endpoint [DHIS2-18451] (#19447) * feat: add optionset to dataItems endpoint [DHIS2-18451] Signed-off-by: Giuseppe Nespolino * test: fix [DHIS2-18451] Signed-off-by: Giuseppe Nespolino * test: e2e [DHIS2-18451] Signed-off-by: Giuseppe Nespolino * test: e2e [DHIS2-18451] Signed-off-by: Giuseppe Nespolino * test: e2e [DHIS2-18451] Signed-off-by: Giuseppe Nespolino --------- Signed-off-by: Giuseppe Nespolino --- .../hisp/dhis/common/DimensionItemType.java | 3 +- .../dhis/dataitem/query/OptionSetQuery.java | 208 ++++++++++++++++++ .../dataitem/query/QueryableDataItem.java | 4 +- .../shared/NameTranslationStatement.java | 25 ++- .../dataitems/DataItemsAnalyticsTest.java | 77 +++++++ 5 files changed, 310 insertions(+), 7 deletions(-) create mode 100644 dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataitem/query/OptionSetQuery.java create mode 100644 dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/analytics/dataitems/DataItemsAnalyticsTest.java diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/DimensionItemType.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/DimensionItemType.java index c47aeca4d5b2..1e4a96cc7473 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/DimensionItemType.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/DimensionItemType.java @@ -46,5 +46,6 @@ public enum DimensionItemType { ORGANISATION_UNIT_GROUP, CATEGORY_OPTION_GROUP, EXPRESSION_DIMENSION_ITEM, - SUBEXPRESSION_DIMENSION_ITEM + SUBEXPRESSION_DIMENSION_ITEM, + OPTION_SET } diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataitem/query/OptionSetQuery.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataitem/query/OptionSetQuery.java new file mode 100644 index 000000000000..2d8d88d552af --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataitem/query/OptionSetQuery.java @@ -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 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(); + } +} diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataitem/query/QueryableDataItem.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataitem/query/QueryableDataItem.java index cd8077fd1257..d8ac362e61f3 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataitem/query/QueryableDataItem.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataitem/query/QueryableDataItem.java @@ -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; @@ -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 entity; diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataitem/query/shared/NameTranslationStatement.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataitem/query/shared/NameTranslationStatement.java index 5bf0fa51388f..e5618a13a658 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataitem/query/shared/NameTranslationStatement.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataitem/query/shared/NameTranslationStatement.java @@ -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)) { @@ -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"); } @@ -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 @@ -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"); diff --git a/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/analytics/dataitems/DataItemsAnalyticsTest.java b/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/analytics/dataitems/DataItemsAnalyticsTest.java new file mode 100644 index 000000000000..be1bbcfba11c --- /dev/null +++ b/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/analytics/dataitems/DataItemsAnalyticsTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2004-2023, 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.analytics.dataitems; + +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.Matchers.equalTo; + +import org.hisp.dhis.helpers.extensions.ConfigurationExtension; +import org.hisp.dhis.test.e2e.actions.LoginActions; +import org.hisp.dhis.test.e2e.actions.RestApiActions; +import org.hisp.dhis.test.e2e.dto.ApiResponse; +import org.hisp.dhis.test.e2e.helpers.QueryParamsBuilder; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * This test class has to run before all tests, because the scenarios are testing errors related to + * missing analytics tables. For this reason they have to run first (before analytics tables are + * created), hence @Order(1). + */ +@Order(1) +@ExtendWith(ConfigurationExtension.class) +@Tag("analytics") +public class DataItemsAnalyticsTest { + + private final RestApiActions dataItemsActions = new RestApiActions("/dataItems"); + + @BeforeAll + public static void beforeAll() { + new LoginActions().loginAsAdmin(); + } + + @Test + void testDataItemsContainsOptionSets() { + // Given + QueryParamsBuilder params = new QueryParamsBuilder().add("paging=false").add("order=name:asc"); + + // When + ApiResponse response = dataItemsActions.get(params); + + // Then + response + .validate() + .statusCode(equalTo(200)) + .body("dataItems.dimensionItemType", hasItem("PROGRAM_INDICATOR")) + .body("dataItems.dimensionItemType", hasItem("DATA_ELEMENT")) + .body("dataItems.dimensionItemType", hasItem("OPTION_SET")); + } +}