Skip to content

Commit 960a2c7

Browse files
committed
merge: resolve master conflict after dhis2#24803
dhis2#24803 already landed the fuller nativeSynchronizedQuery fix for HibernateJobConfigurationStore, including the FileResource query space. Drop the temporary local synchronizedNativeQuery subset from this branch and keep the PR's ehcache hot-region predefinitions.
2 parents 4ffddfa + 38bab12 commit 960a2c7

39 files changed

Lines changed: 2206 additions & 80 deletions

File tree

dhis-2/dhis-services/dhis-service-administration/src/main/resources/data-integrity-checks.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ checks:
5050
- program_rules/program_rules_no_priority.yaml
5151
- program_rules/program_rules_inconsistent_program_program_stage.yaml
5252
- analytical_objects/visualizations_not_used_1year.yaml
53+
- analytical_objects/visualizations_wrong_sort_order.yaml
5354
- analytical_objects/maps_not_used_1year.yaml
5455
- analytical_objects/dashboards_not_used_1year.yaml
5556
- analytical_objects/dashboards_empty.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Copyright (c) 2004-2022, University of Oslo
2+
# All rights reserved.
3+
#
4+
# Redistribution and use in source and binary forms, with or without
5+
# modification, are permitted provided that the following conditions are met:
6+
# Redistributions of source code must retain the above copyright notice, this
7+
# list of conditions and the following disclaimer.
8+
#
9+
# Redistributions in binary form must reproduce the above copyright notice,
10+
# this list of conditions and the following disclaimer in the documentation
11+
# and/or other materials provided with the distribution.
12+
# Neither the name of the HISP project nor the names of its contributors may
13+
# be used to endorse or promote products derived from this software without
14+
# specific prior written permission.
15+
#
16+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17+
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18+
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
20+
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21+
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22+
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
23+
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26+
#
27+
---
28+
name: visualizations_wrong_sort_order
29+
description: Visualizations with possibly wrong sort order of data dimension items
30+
section: Visualizations
31+
section_order: 2
32+
summary_sql: >-
33+
WITH visualizations_wrong_sort_order AS (
34+
SELECT DISTINCT visualizationid, sort_order, expected_order
35+
FROM (
36+
SELECT
37+
visualizationid,
38+
sort_order,
39+
row_number() OVER (PARTITION BY visualizationid ORDER BY sort_order)
40+
+ (MIN(sort_order) OVER (PARTITION BY visualizationid) - 1) AS expected_order
41+
FROM visualization_datadimensionitems
42+
) AS foo
43+
WHERE sort_order != expected_order
44+
ORDER BY visualizationid, sort_order
45+
)
46+
SELECT
47+
COUNT(*) AS value,
48+
100 * (SELECT COUNT(*) FROM (SELECT DISTINCT visualizationid FROM visualizations_wrong_sort_order) AS bar) /
49+
NULLIF((SELECT COUNT(*) FROM visualization), 0) AS percent
50+
FROM visualizations_wrong_sort_order;
51+
details_sql: >-
52+
SELECT
53+
a.uid,
54+
a.name,
55+
b.sort_order || ' != ' || b.expected_order AS comment
56+
FROM visualization a
57+
INNER JOIN (
58+
SELECT DISTINCT visualizationid, sort_order, expected_order
59+
FROM (
60+
SELECT
61+
visualizationid,
62+
sort_order,
63+
row_number() OVER (PARTITION BY visualizationid ORDER BY sort_order)
64+
+ (MIN(sort_order) OVER (PARTITION BY visualizationid) - 1) AS expected_order
65+
FROM visualization_datadimensionitems
66+
) AS foo
67+
WHERE sort_order != expected_order
68+
ORDER BY visualizationid, sort_order
69+
) b ON a.visualizationid = b.visualizationid;
70+
details_id_type: visualizations
71+
severity: CRITICAL
72+
introduction: >
73+
The data dimension items within a visualization are ordered using a sort order column. If
74+
this sort order contains gaps (for example 0, 1, 3, 4 instead of 0, 1, 2, 3), the visualization
75+
will fail to open with an internal server error: the sort order is used as a list index when
76+
the visualization is loaded, and a gap causes the missing position to be reconstructed as an
77+
empty entry, which then causes an error when the visualization's data dimension items are
78+
processed. This check identifies any visualizations where the sort order of the data dimension
79+
items contains gaps.
80+
recommendation: >
81+
To correct the sort order of a visualization's data dimension items, an administrator can
82+
directly update the sort_order column of the affected rows in the visualization_datadimensionitems
83+
table so that it forms a contiguous sequence starting at 0, with no gaps or duplicates, for each
84+
affected visualization.

dhis-2/dhis-services/dhis-service-administration/src/test/java/org/hisp/dhis/dataintegrity/DataIntegrityYamlReaderTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ void testReadDataIntegrityYaml() {
9393

9494
List<DataIntegrityCheck> checks = new ArrayList<>();
9595
readYaml(checks, "data-integrity-checks.yaml", "data-integrity-checks", CLASS_PATH);
96-
assertEquals(95, checks.size());
96+
assertEquals(96, checks.size());
9797

9898
// Names should be unique
9999
List<String> allNames = checks.stream().map(DataIntegrityCheck::getName).toList();

dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/common/query/jsonextractor/SqlRowSetJsonExtractorDelegator.java

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,42 @@ public class SqlRowSetJsonExtractorDelegator extends SqlRowSetDelegator {
9292
OBJECT_MAPPER.findAndRegisterModules();
9393
}
9494

95+
/** Package-private so that a test can count how often the document is actually parsed. */
9596
@SneakyThrows
96-
private List<JsonEnrollment> parseEnrollmentsFromJson(String json) {
97+
List<JsonEnrollment> parseEnrollmentsFromJson(String json) {
9798
return OBJECT_MAPPER.readValue(json, new TypeReference<>() {});
9899
}
99100

101+
/**
102+
* Returns the parsed {@code enrollments} document for the row the cursor is on, parsing it at
103+
* most once per row.
104+
*
105+
* <p>Every requested column used to trigger its own {@code readValue} of the whole document, from
106+
* two independent call sites ({@link #getObject(String)} and {@link #getRowContextItem(String,
107+
* int)}), so a line list with ten data-element dimensions parsed the same string around twenty
108+
* times per row.
109+
*
110+
* <p>The memo is keyed on the document itself rather than on the cursor position, which is what
111+
* makes a stale hit impossible rather than merely unlikely: a hit requires the current row's
112+
* document to be equal to the memoised one, and equal input cannot produce a different parse
113+
* result. {@link String#equals} short-circuits on identity, which is the case a forward cursor
114+
* hits on every column after the first.
115+
*
116+
* <p>The returned list is shared between the callers within a row. Nothing downstream mutates it
117+
* - the extractors only stream over it, and {@code getItemBasedOnOffset} sorts the stream, not
118+
* the source.
119+
*/
120+
private List<JsonEnrollment> getEnrollments() {
121+
String json = super.getString("enrollments");
122+
123+
if (memoisedEnrollments == null || !Objects.equals(memoisedJson, json)) {
124+
memoisedEnrollments = parseEnrollmentsFromJson(json);
125+
memoisedJson = json;
126+
}
127+
128+
return memoisedEnrollments;
129+
}
130+
100131
private static final Comparator<JsonEnrollment> ENR_ENROLLMENT_DATE_COMPARATOR =
101132
comparing(JsonEnrollment::getEnrollmentDate, nullsFirst(naturalOrder())).reversed();
102133

@@ -107,6 +138,14 @@ private List<JsonEnrollment> parseEnrollmentsFromJson(String json) {
107138

108139
private final List<String> existingColumnsInRowSet;
109140

141+
/**
142+
* The {@code enrollments} document of the row the cursor is on, and its parse. Per-instance state
143+
* on a per-request object; this class is not shared between threads and must not become so.
144+
*/
145+
private String memoisedJson;
146+
147+
private List<JsonEnrollment> memoisedEnrollments;
148+
110149
public SqlRowSetJsonExtractorDelegator(
111150
SqlRowSet sqlRowSet, List<DimensionIdentifier<DimensionParam>> dimensionIdentifiers) {
112151
super(sqlRowSet);
@@ -179,7 +218,7 @@ public Object getObject(String columnLabel) throws InvalidResultSetAccessExcepti
179218
return super.getObject(canonicalLabel);
180219
}
181220
// if the column is not present in the rowset, we check if it is present in the json string
182-
List<JsonEnrollment> enrollments = parseEnrollmentsFromJson(super.getString("enrollments"));
221+
List<JsonEnrollment> enrollments = getEnrollments();
183222

184223
DimensionIdentifier<DimensionParam> dimensionIdentifier = dimIdByKey.get(canonicalLabel);
185224
if (dimensionIdentifier == null) {
@@ -350,8 +389,7 @@ public Map<String, Object> getRowContextItem(String columnName, int rowIndex) {
350389
}
351390

352391
JsonEvent event =
353-
getJsonEnrollment(
354-
parseEnrollmentsFromJson(super.getString("enrollments")), dimensionIdentifier)
392+
getJsonEnrollment(getEnrollments(), dimensionIdentifier)
355393
.map(jEnr -> getJsonEvent(dimensionIdentifier, jEnr))
356394
.orElse(null);
357395

dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/EventAnalyticsUtils.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,13 @@ public static Map<String, Object> getAggregatedEventDataMapping(Grid grid) {
145145
* Adds values.
146146
*
147147
* @param identifiers the list of list of identifiers.
148-
* @param grid the input {@link Grid}.
148+
* @param valueMap the value map of the input {@link Grid}, as returned by {@link
149+
* #getAggregatedEventDataMapping(Grid)}. It depends only on the input grid, so callers which
150+
* invoke this method repeatedly for the same grid must build it once and reuse it.
149151
* @param outputGrid the output {@link Grid}.
150152
*/
151-
public static void addValues(List<List<String>> identifiers, Grid grid, Grid outputGrid) {
152-
Map<String, Object> valueMap = getAggregatedEventDataMapping(grid);
153-
153+
public static void addValues(
154+
List<List<String>> identifiers, Map<String, Object> valueMap, Grid outputGrid) {
154155
boolean hasValues = false;
155156

156157
for (List<String> idList : identifiers) {

dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/EventAggregateService.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import static org.hisp.dhis.analytics.common.ColumnHeader.PROGRAM_STATUS;
5050
import static org.hisp.dhis.analytics.event.EventAnalyticsUtils.addValues;
5151
import static org.hisp.dhis.analytics.event.EventAnalyticsUtils.generateEventDataPermutations;
52+
import static org.hisp.dhis.analytics.event.EventAnalyticsUtils.getAggregatedEventDataMapping;
5253
import static org.hisp.dhis.analytics.event.LabelMapper.getEnrollmentDateLabel;
5354
import static org.hisp.dhis.analytics.event.LabelMapper.getIncidentDateLabel;
5455
import static org.hisp.dhis.analytics.event.LabelMapper.getOrgUnitLabel;
@@ -636,6 +637,10 @@ private Grid generateOutputGrid(
636637
outputGrid.addHeader(new GridHeader(display, display, NUMBER, false, false));
637638
});
638639

640+
// The value map is a pure function of the input grid, which is not modified below. Build it
641+
// once here instead of once per row permutation.
642+
Map<String, Object> valueMap = getAggregatedEventDataMapping(grid);
643+
639644
for (Map<String, EventAnalyticsDimensionalItem> rowCombination : rowPermutations) {
640645
outputGrid.addRow();
641646
List<List<String>> ids = new ArrayList<>();
@@ -663,7 +668,7 @@ private Grid generateOutputGrid(
663668
}
664669

665670
addValuesInOutputGrid(rowDimensions, outputGrid, displayObjects, params);
666-
addValues(ids, grid, outputGrid);
671+
addValues(ids, valueMap, outputGrid);
667672
}
668673

669674
return getGridWithRows(grid, outputGrid);
@@ -684,19 +689,19 @@ private static Grid getGridWithRows(Grid grid, Grid outputGrid) {
684689
* empty.
685690
*
686691
* @param rowDimensions the list of row dimensions.
687-
* @param grid the {@link Grid}.
692+
* @param outputGrid the output {@link Grid}.
688693
* @param displayObjects the map of display objects.
689694
* @param params the {@link EventQueryParams}.
690695
*/
691696
private static void addValuesInOutputGrid(
692697
List<String> rowDimensions,
693-
Grid grid,
698+
Grid outputGrid,
694699
Map<String, EventAnalyticsDimensionalItem> displayObjects,
695700
EventQueryParams params) {
696701
if (!displayObjects.isEmpty()) {
697702
rowDimensions.forEach(
698703
dimension ->
699-
grid.addValue(
704+
outputGrid.addValue(
700705
displayObjects.get(dimension).getDisplayProperty(params.getDisplayProperty())));
701706
}
702707
}

0 commit comments

Comments
 (0)