From d8119c0f80c73ac5218b48f13209f02faefb08c3 Mon Sep 17 00:00:00 2001 From: Luciano Fiandesio Date: Fri, 20 Dec 2024 16:19:22 +0100 Subject: [PATCH] WIP - various fixes --- .../dhis/analytics/common/CTEContext.java | 128 ++++++- .../analytics/common/RowContextUtils.java | 2 +- .../AbstractJdbcEventAnalyticsManager.java | 8 +- .../data/JdbcEnrollmentAnalyticsManager.java | 348 +++++++++--------- ...efaultProgramIndicatorSubqueryBuilder.java | 3 +- 5 files changed, 293 insertions(+), 196 deletions(-) diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/common/CTEContext.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/common/CTEContext.java index 82c4eacff75..6695d77aa33 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/common/CTEContext.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/common/CTEContext.java @@ -27,23 +27,46 @@ */ package org.hisp.dhis.analytics.common; +import lombok.Getter; +import org.apache.commons.text.RandomStringGenerator; +import org.hisp.dhis.common.QueryItem; +import org.hisp.dhis.program.ProgramIndicator; +import org.hisp.dhis.program.ProgramStage; + import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; -import lombok.Getter; public class CTEContext { - private final Map cteDefinitions = new LinkedHashMap<>(); - private final Map columnMappings = new HashMap<>(); + private final Map cteDefinitions = new LinkedHashMap<>(); @Getter private final Map rowContextReferences = new HashMap<>(); - public void addCTE(String cteName, String cteDefinition) { - cteDefinitions.put(cteName, cteDefinition); + + public CteDefinitionWithOffset getDefinitionByItemUid(String itemUid) { + return cteDefinitions.get(itemUid); + } + + public void addCTE(ProgramStage programStage, QueryItem item, String cteDefinition, int offset) { + cteDefinitions.put(item.getItem().getUid(), + new CteDefinitionWithOffset(programStage.getUid(), cteDefinition, offset)); + } + + public void addCTE(ProgramStage programStage, QueryItem item, String cteDefinition, int offset, boolean isRowContext) { + cteDefinitions.put(item.getItem().getUid(), new CteDefinitionWithOffset(programStage.getUid(), cteDefinition, offset, isRowContext)); } - public void addColumnMapping(String originalColumn, String cteReference) { - columnMappings.put(originalColumn, cteReference); + /** + * Adds a CTE definition to the context. + * @param programIndicator The program indicator + * @param cteDefinition The CTE definition (the SQL query) + */ + public void addCTE(ProgramIndicator programIndicator, String cteDefinition) { + cteDefinitions.put(programIndicator.getUid(), new CteDefinitionWithOffset(programIndicator.getUid(), cteDefinition)); + } + + public void addCTEFilter(String name, String ctedefinition) { + cteDefinitions.put(name, new CteDefinitionWithOffset(name, ctedefinition, true)); } /** @@ -64,25 +87,100 @@ public String getCTEDefinition() { StringBuilder sb = new StringBuilder("WITH "); boolean first = true; - for (Map.Entry entry : cteDefinitions.entrySet()) { + for (Map.Entry entry : cteDefinitions.entrySet()) { if (!first) { sb.append(", "); } - sb.append(entry.getKey()).append(" AS (").append(entry.getValue()).append(")"); + CteDefinitionWithOffset cteDef = entry.getValue(); + sb.append(cteDef.asCteName(entry.getKey())).append(" AS (").append(entry.getValue().cteDefinition).append(")"); first = false; } return sb.toString(); } - + // Rename to item uid public Set getCTENames() { return cteDefinitions.keySet(); } - - public String getColumnMapping(String columnId) { - return columnMappings.getOrDefault(columnId, columnId); - } - + public boolean containsCteFilter(String cteFilterName) { return cteDefinitions.containsKey(cteFilterName); } + + @Getter + public static class CteDefinitionWithOffset { + // The program stage uid + private final String programStageUid; + // The program indicator uid + private String programIndicatorUid; + // The CTE definition (the SQL query) + private final String cteDefinition; + // The calculated offset + private final int offset; + // The alias of the CTE + private final String alias; + // Whether the CTE is a row context (TODO this need a better explanation) + private boolean isRowContext; + // Whether the CTE is a program indicator + private boolean isProgramIndicator = false; + // Whether the CTE is a filter + private boolean isFilter = false; + private final static String PS_PREFIX = "ps"; + private final static String PI_PREFIX = "pi"; + + public CteDefinitionWithOffset(String programStageUid, String cteDefinition, int offset) { + this.programStageUid = programStageUid; + this.cteDefinition = cteDefinition; + this.offset = offset; + this.alias = new RandomStringGenerator.Builder().withinRange('a', 'z').build() + .generate(5); + this.isRowContext = false; + } + + public CteDefinitionWithOffset(String programStageUid, String cteDefinition, int offset, boolean isRowContext) { + this(programStageUid, cteDefinition, offset); + this.isRowContext = isRowContext; + } + + public CteDefinitionWithOffset(String programIndicatorUid, String cteDefinition) { + this.cteDefinition = cteDefinition; + this.programIndicatorUid = programIndicatorUid; + this.programStageUid = null; + this.offset = -999; + this.alias = new RandomStringGenerator.Builder().withinRange('a', 'z').build() + .generate(5); + this.isRowContext = false; + this.isProgramIndicator = true; + } + + public CteDefinitionWithOffset(String cteFilterName, String cteDefinition, boolean isFilter) { + this.cteDefinition = cteDefinition; + this.programIndicatorUid = null; + this.programStageUid = null; + this.offset = -999; + this.alias = new RandomStringGenerator.Builder().withinRange('a', 'z').build() + .generate(5); + this.isRowContext = false; + this.isProgramIndicator = false; + this.isFilter = isFilter; + } + + /** + * + * @param uid the uid of an dimension item or ProgramIndicator + * @return the name of the CTE + */ + public String asCteName(String uid) { + if (isProgramIndicator) { + return "%s_%s".formatted(PI_PREFIX, programIndicatorUid.toLowerCase()); + } + if (isFilter) { + return "%s".formatted(uid.toLowerCase()); + } + return "%s_%s_%s".formatted(PS_PREFIX, programStageUid.toLowerCase(), uid.toLowerCase()); + } + + public boolean isProgramStage() { + return !isFilter && !isProgramIndicator; + } + } } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/common/RowContextUtils.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/common/RowContextUtils.java index d6341b17682..d2753a7547e 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/common/RowContextUtils.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/common/RowContextUtils.java @@ -50,7 +50,7 @@ public static List getRowContextWhereClauses(CTEContext cteContext) { Map rowCtxRefs = cteContext.getRowContextReferences(); for (String alias : rowCtxRefs.values()) { whereClauses.add("%s.value is null".formatted(alias)); - whereClauses.add("%s.exists_flag = true".formatted(alias)); + //whereClauses.add("%s.exists_flag = true".formatted(alias)); } return whereClauses; } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/AbstractJdbcEventAnalyticsManager.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/AbstractJdbcEventAnalyticsManager.java index f4ff6f8f2c4..2e22c86905c 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/AbstractJdbcEventAnalyticsManager.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/AbstractJdbcEventAnalyticsManager.java @@ -92,6 +92,8 @@ import org.hisp.dhis.analytics.SortOrder; import org.hisp.dhis.analytics.analyze.ExecutionPlanStore; import org.hisp.dhis.analytics.common.CTEContext; +import org.hisp.dhis.analytics.common.CTEContext.CteDefinitionWithOffset; +import org.hisp.dhis.analytics.common.CTEUtils; import org.hisp.dhis.analytics.common.ProgramIndicatorSubqueryBuilder; import org.hisp.dhis.analytics.event.EventQueryParams; import org.hisp.dhis.analytics.util.AnalyticsUtils; @@ -1403,8 +1405,10 @@ protected List getSelectColumnsWithCTE(EventQueryParams params, CTEConte if (queryItem.isProgramIndicator()) { // For program indicators, use CTE reference String piUid = queryItem.getItem().getUid(); - String cteReference = cteContext.getColumnMapping(piUid); - columns.add(cteReference + " as \"" + piUid + "\""); + CteDefinitionWithOffset cteDef = cteContext.getDefinitionByItemUid(piUid); + // ugaee.value as "CH6wamtY9kK", + String col = "%s.value as %s".formatted(cteDef.getAlias(), piUid); + columns.add(col); } else if (ValueType.COORDINATE == queryItem.getValueType()) { // Handle coordinates columns.add(getCoordinateColumn(queryItem).asSql()); diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/JdbcEnrollmentAnalyticsManager.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/JdbcEnrollmentAnalyticsManager.java index 09adfaf515f..88d51872d3b 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/JdbcEnrollmentAnalyticsManager.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/JdbcEnrollmentAnalyticsManager.java @@ -44,6 +44,8 @@ import static org.hisp.dhis.util.DateUtils.toMediumDate; import com.google.common.collect.Sets; + +import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashMap; @@ -56,6 +58,7 @@ import org.apache.commons.lang3.StringUtils; import org.hisp.dhis.analytics.analyze.ExecutionPlanStore; import org.hisp.dhis.analytics.common.CTEContext; +import org.hisp.dhis.analytics.common.CTEContext.CteDefinitionWithOffset; import org.hisp.dhis.analytics.common.CTEUtils; import org.hisp.dhis.analytics.common.ProgramIndicatorSubqueryBuilder; import org.hisp.dhis.analytics.common.RowContextUtils; @@ -504,6 +507,15 @@ private String addFiltersToWhereClause(EventQueryParams params) { return getQueryItemsAndFiltersWhereClause(params, new SqlHelper()); } + private String addRowContextFilters(CTEContext cteContext) { + + List rowContextColumns = RowContextUtils.getRowContextWhereClauses(cteContext); + if (!rowContextColumns.isEmpty()) { + return String.join(" AND ", rowContextColumns); + } + return EMPTY; + } + private String addCteFiltersToWhereClause(EventQueryParams params, CTEContext cteContext) { StringBuilder whereClause = new StringBuilder(); @@ -516,15 +528,16 @@ private String addCteFiltersToWhereClause(EventQueryParams params, CTEContext ct String cteName = CTEUtils.createFilterName(item); if (cteContext.containsCteFilter(cteName)) { + CteDefinitionWithOffset cteDef = cteContext.getDefinitionByItemUid(cteName); for (QueryFilter filter : item.getFilters()) { if ("NV".equals(filter.getFilter())) { // Handle null filters explicitly - whereClause.append(" AND ").append(cteName).append(".value IS NULL"); + whereClause.append(" AND ").append(cteDef.getAlias()).append(".value IS NULL"); } else { String operator = getSqlOperator(filter); String value = getSqlFilterValue(filter, item); whereClause .append(" AND ") - .append(cteName) + .append(cteDef.getAlias()) .append(".value ") .append(operator) .append(" ") @@ -543,19 +556,6 @@ private String addCteFiltersToWhereClause(EventQueryParams params, CTEContext ct } } - // Handle the row context case - List rowContextColumns = RowContextUtils.getRowContextWhereClauses(cteContext); - if (rowContextColumns.isEmpty()) { - return whereClause.toString(); - } else { - if (whereClause.isEmpty()) { - whereClause.append(" where "); - } else { - whereClause.append(" AND "); - } - whereClause.append(String.join(" AND ", rowContextColumns)); - } - return whereClause.toString(); } @@ -713,78 +713,32 @@ protected ColumnAndAlias getCoordinateColumn(QueryItem item, String suffix) { } protected String getColumnWithCte(QueryItem item, String suffix, CTEContext cteContext) { + List columns = new ArrayList<>(); String colName = item.getItemName(); - String alias; - if (item.hasProgramStage()) { - assertProgram(item); - colName = quote(colName + suffix); + CteDefinitionWithOffset cteDef = cteContext.getDefinitionByItemUid(item.getItem().getUid()); - // Generate CTE name based on program stage and item - String cteName = - String.format( - "ps_%s_%s", - item.getProgramStage().getUid().toLowerCase(), item.getItem().getUid().toLowerCase()); + String alias = getAlias(item).orElse(null); - String eventTableName = ANALYTICS_EVENT + item.getProgram().getUid(); - String excludingScheduledCondition = - eventTableName + ".eventstatus != '" + EventStatus.SCHEDULE + "'"; + // ed."lJTx9EZ1dk1" as "EPEcjy3FWmI[-1].lJTx9EZ1dk1" - if (item.getProgramStage().getRepeatable() - && item.hasRepeatableStageParams() - && !item.getRepeatableStageParams().simpleStageValueExpected()) { + columns.add(""" + %s.%s as %s + """.formatted(cteDef.getAlias(), quote(colName), quote(alias))); + if (cteDef.isRowContext()) { + // Add additional status and exists columns for row context + // (ed."lJTx9EZ1dk1" IS NOT NULL) as "EPEcjy3FWmI[-1].lJTx9EZ1dk1.exists", + // ed.eventstatus as "EPEcjy3FWmI[-1].lJTx9EZ1dk1.status" + columns.add(""" + (%s.%s IS NOT NULL) as %s + """.formatted(cteDef.getAlias(), quote(colName), quote(alias + ".exists"))); - String cteSql = - String.format( - "SELECT enrollment, COALESCE(json_agg(t1), '[]') as value FROM (" - + " SELECT %s, %s, %s, %s" - + " FROM %s" - + " WHERE %s AND ps = '%s' %s %s %s %s" - + ") as t1 GROUP BY enrollment", - colName, - String.join( - ", ", - EventAnalyticsColumnName.ENROLLMENT_OCCURRED_DATE_COLUMN_NAME, - EventAnalyticsColumnName.SCHEDULED_DATE_COLUMN_NAME, - EventAnalyticsColumnName.OCCURRED_DATE_COLUMN_NAME), - eventTableName, - excludingScheduledCondition, - item.getProgramStage().getUid(), - getExecutionDateFilter( - item.getRepeatableStageParams().getStartDate(), - item.getRepeatableStageParams().getEndDate()), - createOrderType(item.getProgramStageOffset()), - createOffset(item.getProgramStageOffset()), - getLimit(item.getRepeatableStageParams().getCount())); - - cteContext.addCTE(cteName, cteSql); - alias = quote(item.getProgramStage().getUid() + "." + item.getItem().getUid()); - return cteName + ".value as " + alias; + columns.add(""" + %s.eventstatus as %s + """.formatted(cteDef.getAlias(), quote(alias + ".status"))); - } else { - String cteSql = - String.format( - "SELECT DISTINCT ON (enrollment) enrollment, %s as value " - + "FROM %s " - + "WHERE %s AND ps = '%s' " - + "ORDER BY enrollment, occurreddate DESC, created DESC", - colName, - eventTableName, - excludingScheduledCondition, - item.getProgramStage().getUid()); - - cteContext.addCTE(cteName, cteSql); - String columnAlias = quote(item.getProgramStage().getUid() + "." + item.getItem().getUid()); - return cteName + ".value as " + columnAlias; - } - } - - // Non-program stage cases remain unchanged - if (isOrganizationUnitProgramAttribute(item)) { - return quoteAlias(colName + suffix); - } else { - return quoteAlias(colName); } + return String.join(", ", columns); } /** @@ -986,18 +940,6 @@ private String createOffset(int offset) { } } - private int createOffset2(int offset) { - if (offset == 0) { - return 0; - } - - if (offset < 0) { - return (-1 * offset); - } else { - return (offset - 1); - } - } - private String createOrderType(int offset) { if (offset == 0) { return ORDER_BY_EXECUTION_DATE.replace(DIRECTION_PLACEHOLDER, "desc"); @@ -1011,89 +953,112 @@ private String createOrderType(int offset) { // New methods // - private CTEContext getCteDefinitions(EventQueryParams params) { - CTEContext cteContext = new CTEContext(); - - for (QueryItem queryItem : params.getItems()) { - if (queryItem.isProgramIndicator()) { - ProgramIndicator pi = (ProgramIndicator) queryItem.getItem(); - - if (queryItem.hasRelationshipType()) { - programIndicatorSubqueryBuilder.contributeCTE( + private void handleProgramIndicatorCte(QueryItem item, CTEContext cteContext, EventQueryParams params) { + ProgramIndicator pi = (ProgramIndicator) item.getItem(); + if (item.hasRelationshipType()) { + programIndicatorSubqueryBuilder.contributeCTE( pi, - queryItem.getRelationshipType(), + item.getRelationshipType(), getAnalyticsType(), params.getEarliestStartDate(), params.getLatestEndDate(), cteContext); - } else { - programIndicatorSubqueryBuilder.contributeCTE( + } else { + programIndicatorSubqueryBuilder.contributeCTE( pi, getAnalyticsType(), params.getEarliestStartDate(), params.getLatestEndDate(), cteContext); - } - } else if (queryItem.hasProgramStage() && queryItem.hasProgram()) { - // Generate CTE for program stage items - String cteName = - String.format( - "ps_%s_%s", - queryItem.getProgramStage().getUid().toLowerCase(), - queryItem.getItem().getUid().toLowerCase()); - - String colName = quote(queryItem.getItemName()); - String eventTableName = ANALYTICS_EVENT + queryItem.getProgram().getUid(); - - String cteSql = - """ - -- Generate CTE for program stage items - SELECT DISTINCT ON (enrollment) enrollment, %s as value%s - FROM %s - WHERE eventstatus != 'SCHEDULE' - AND ps = '%s' - ORDER BY enrollment, occurreddate DESC, created DESC""" - .formatted( - colName, - rowContextAllowedAndNeeded(params, queryItem) ? " ,true as exists_flag" : "", - eventTableName, - queryItem.getProgramStage().getUid()); - - cteContext.addCTE(cteName, cteSql); - String unquotedAlias = - queryItem.getProgramStage().getUid() + "." + queryItem.getItem().getUid(); - cteContext.addColumnMapping( - queryItem.getProgramStage().getUid() + "." + queryItem.getItem().getUid(), - cteName + ".value as " + quote(unquotedAlias)); - - if (rowContextAllowedAndNeeded( - params, queryItem)) { // TODO original condition && !isEmpty(columnAndAlias.alias)) { - - String statusCteSql = - """ - select - distinct on (enrollment) - enrollment, - eventstatus as status - from - %s - where - eventstatus != 'SCHEDULE' - and ps = '%s' - order by - enrollment, - occurreddate desc, - created desc - """ - .formatted(eventTableName, queryItem.getProgramStage().getUid()); - cteContext.addCTE(cteName + "_status", statusCteSql); - cteContext.addRowContextColumnMapping(unquotedAlias, cteName); + } + } + + private CTEContext getCteDefinitions(EventQueryParams params) { + CTEContext cteContext = new CTEContext(); + + for (QueryItem item : params.getItems()) { + + String itemId = item.getItem().getUid(); + + String eventTableName = ANALYTICS_EVENT + item.getProgram().getUid(); + if (item.isProgramIndicator()) { + handleProgramIndicatorCte(item, cteContext, params); + } else if(item.hasProgramStage()) { + // TODO what is this condition would be good to give it a name + if (item.getProgramStage().getRepeatable() + && item.hasRepeatableStageParams() + && !item.getRepeatableStageParams().simpleStageValueExpected()) { + + // TODO: Implement repeatable stage items + log.warn("Repeatable stage items are not yet supported"); + // TODO what is this condition - would be good to give it a name + } else if (item.getProgramStage().getRepeatable() && item.hasRepeatableStageParams()) { + String colName = quote(item.getItemName()); + boolean hasEventStatusColumn = rowContextAllowedAndNeeded(params, item); + + var cteSql = """ + SELECT + enrollment, + %s,%s + ROW_NUMBER() OVER ( + PARTITION BY enrollment + ORDER BY occurreddate DESC, created DESC + ) as rn + FROM %s + WHERE eventstatus != 'SCHEDULE' + AND ps = '%s' + """.formatted( + colName, + hasEventStatusColumn ? " eventstatus," : "", + eventTableName, + item.getProgramStage().getUid()); + + cteContext.addCTE( + item.getProgramStage(), + item, + cteSql, + createOffset2(item.getProgramStageOffset()), + hasEventStatusColumn + ); + } else { + + // Generate CTE for program stage items + String colName = quote(item.getItemName()); + + String cteSql = + """ + -- Generate CTE for program stage items + SELECT DISTINCT ON (enrollment) enrollment, %s as value%s + FROM %s + WHERE eventstatus != 'SCHEDULE' + AND ps = '%s' + ORDER BY enrollment, occurreddate DESC, created DESC %s %s""" + .formatted( + colName, + rowContextAllowedAndNeeded(params, item) ? " ,true as exists_flag" : "", + eventTableName, + item.getProgramStage().getUid(), + createOffset(item.getProgramStageOffset()), LIMIT_1); + + cteContext.addCTE(item.getProgramStage(), item, cteSql, createOffset2(item.getProgramStageOffset())); } } } return cteContext; } + private int createOffset2(int offset) { + if (offset == 0) { + return 0; + } + + if (offset < 0) { + return (-1 * offset); + } else { + return (offset - 1); + } + } + private void generateFilterCTEs(EventQueryParams params, CTEContext cteContext) { // Combine items and item filters List queryItems = @@ -1118,7 +1083,7 @@ private void generateFilterCTEs(EventQueryParams params, CTEContext cteContext) (identifier, items) -> { String cteName = createFilterNameByIdentifier(identifier); String cteSql = buildFilterCteSql(items, params); - cteContext.addCTE(cteName, cteSql); + cteContext.addCTEFilter(cteName, cteSql); }); // Process non-repeatable stage filters @@ -1129,13 +1094,13 @@ private void generateFilterCTEs(EventQueryParams params, CTEContext cteContext) if (queryItem.hasProgram() && queryItem.hasProgramStage()) { String cteName = CTEUtils.createFilterName(queryItem); String cteSql = buildFilterCteSql(List.of(queryItem), params); - cteContext.addCTE(cteName, cteSql); + cteContext.addCTEFilter(cteName, cteSql); } }); } private String buildEnrollmentQueryWithCte(EventQueryParams params) { - + // LUCIANO // StringBuilder sql = new StringBuilder(); // 1. Process all program indicators to generate CTEs @@ -1154,27 +1119,35 @@ private String buildEnrollmentQueryWithCte(EventQueryParams params) { List selectCols = ListUtils.distinctUnion( params.isAggregatedEnrollments() ? List.of("enrollment") : COLUMNS, - getSelectColumnsWithCTE(params, cteContext), - getRowContextColumns(cteContext)); - sql.append("SELECT ").append(String.join(",\n ", selectCols)); + getSelectColumnsWithCTE(params, cteContext)); + sql.append("SELECT ").append(String.join(",\n", selectCols)); // 4. From clause sql.append("\nFROM ").append(params.getTableName()).append(" AS ax"); // 5. Add joins for each CTE - for (String cteName : cteContext.getCTENames()) { - sql.append("\nLEFT JOIN ") - .append(cteName) - .append(" ON ax.enrollment = ") - .append(cteName) - .append(".enrollment"); + for (String itemUid : cteContext.getCTENames()) { + CteDefinitionWithOffset cteDef = cteContext.getDefinitionByItemUid(itemUid); + String join = """ + LEFT JOIN %s %s + ON + %s.enrollment = ax.enrollment + """.formatted(cteDef.asCteName(itemUid), cteDef.getAlias(), cteDef.getAlias()); + sql.append("\n").append(join); + if (cteDef.isProgramStage()) { + // equivalent to original OFFSET 1 LIMIT 1 but more efficient + // TODO use constant instead of hardcoded 'rn' column name + String offset = " AND %s.rn = %s".formatted(cteDef.getAlias(), cteDef.getOffset() + 1); + sql.append(offset); + } } // 6. Where clause - sql.append(" ") - .append(getWhereClause(params)) - .append(" ") - .append(addCteFiltersToWhereClause(params, cteContext)); + List conditions = collectWhereConditions(params, cteContext); + if (!conditions.isEmpty()) { + sql.append(" WHERE ") + .append(String.join(" AND ", conditions)); + } // 7. Order by sql.append(" ").append(getSortClause(params)); @@ -1185,10 +1158,33 @@ private String buildEnrollmentQueryWithCte(EventQueryParams params) { return sql.toString(); } - private List getRowContextColumns(CTEContext cteContext) { - return RowContextUtils.getRowContextColumns(cteContext, sqlBuilder); + private List collectWhereConditions(EventQueryParams params, CTEContext cteContext) { + + List conditions = new ArrayList<>(); + + String baseWhereClause = getWhereClause(params).trim(); + String cteFilters = addCteFiltersToWhereClause(params, cteContext).trim(); + String rowContextFilters = addRowContextFilters(cteContext).trim(); + + // Add non-empty conditions + if (!baseWhereClause.isEmpty()) { + // Remove leading WHERE if present + conditions.add(baseWhereClause.replaceFirst("(?i)^WHERE\\s+", "")); + } + if (!cteFilters.isEmpty()) { + conditions.add(cteFilters.replaceFirst("(?i)^AND\\s+", "")); + } + if (!rowContextFilters.isEmpty()) { + conditions.add(rowContextFilters); + } + + return conditions; } +// private List getRowContextColumns(CTEContext cteContext) { +// return RowContextUtils.getRowContextColumns(cteContext, sqlBuilder); +// } + // private String resolveOrderByOffset(int offset) { // // if (offset <= 0) { diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/programindicator/DefaultProgramIndicatorSubqueryBuilder.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/programindicator/DefaultProgramIndicatorSubqueryBuilder.java index 74402819889..18f7c002c8d 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/programindicator/DefaultProgramIndicatorSubqueryBuilder.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/programindicator/DefaultProgramIndicatorSubqueryBuilder.java @@ -134,8 +134,7 @@ public void contributeCTE( latestDate)); // Register the CTE and its column mapping - cteContext.addCTE(cteName, cteSql.toString()); - cteContext.addColumnMapping(programIndicator.getUid(), cteName + ".value"); + cteContext.addCTE(programIndicator, cteSql); } private String getTableName(ProgramIndicator programIndicator) {