Skip to content

feat: support program-stage data element values in TE aggregate analytics [DHIS2-21763]#24387

Open
luciano-fiandesio wants to merge 3 commits into
masterfrom
DHIS2-21763-te-aggregate-event-value-collapse
Open

feat: support program-stage data element values in TE aggregate analytics [DHIS2-21763]#24387
luciano-fiandesio wants to merge 3 commits into
masterfrom
DHIS2-21763-te-aggregate-event-value-collapse

Conversation

@luciano-fiandesio

@luciano-fiandesio luciano-fiandesio commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Extends the tracked-entity aggregate endpoint /api/analytics/trackedEntities/aggregate/{trackedEntityType} so the value parameter can be a program-stage data element (an event value), not just a tracked-entity attribute.

Previously value resolved only to a numeric tracked-entity attribute — a single column on the flat one-row-per-tracked-entity table analytics_te_<tet>. Event data element values live in a separate table (analytics_te_event_<tet>) with many rows per tracked entity (one per event), so they could not be aggregated. This PR reduces each tracked entity's events for the stage to one chosen event, joins it back, and aggregates it — giving correct per-tracked-entity results.

value now accepts programUid.programStageUid.dataElementUid (with an optional stage offset), in addition to the existing bare attribute UID.

What this enables

  • Average birth weight per org unit — from the Birth stage of the Child Programme.
  • Sum / min / max / count of any numeric event data element, grouped by org unit and/or tracked-entity attributes (e.g. total doses administered per org unit; min/max measurement per district).
  • Average birth weight per org unit and gender — the event value combined with an attribute group-by key.
  • Count of tracked entities that have a recorded measurement per org unit — aggregationType=COUNT over the data element counts non-null values (one per tracked entity), not events.
  • Value from a chosen visit — the stage offset picks which occurrence to read: [0] latest (default), [-1] the visit before, [1] the first.

Example queries

# Average latest birth weight (Child Programme, Birth stage) per org unit
GET /api/analytics/trackedEntities/aggregate/nEenWmSyUEp
    ?dimension=ou:QII5GqfDfO3;DiszpKrYNg8
    &value=IpHINAT79UW.A03MvHHogjR.UXz7xuGCEhU
    &aggregationType=AVERAGE

# Sum / min / max of the same value per org unit — swap aggregationType=SUM|MIN|MAX

# Average birth weight per org unit AND gender (attribute group-by key)
GET .../aggregate/nEenWmSyUEp
    ?dimension=ou:QII5GqfDfO3;DiszpKrYNg8,cejWyOfXge6
    &value=IpHINAT79UW.A03MvHHogjR.UXz7xuGCEhU&aggregationType=AVERAGE

# Average birth weight per org unit, filtered to Female (WHERE, not a group-by column)
GET .../aggregate/nEenWmSyUEp
    ?dimension=ou:QII5GqfDfO3;DiszpKrYNg8
    &value=IpHINAT79UW.A03MvHHogjR.UXz7xuGCEhU&aggregationType=AVERAGE
    &filter=cejWyOfXge6:IN:Female

# COUNT of tracked entities with a non-null value per org unit
GET .../aggregate/nEenWmSyUEp?dimension=ou:...&value=<prg.stage.de>&aggregationType=COUNT

# The first recorded value instead of the latest (stage offset)
GET .../aggregate/nEenWmSyUEp?dimension=ou:...&value=<prg>.<stage>[1].<de>&aggregationType=AVERAGE

The response is the same grid shape as the existing aggregate endpoint: one column per grouped dimension (carrying the dimension-item UID) plus a trailing numeric value column; grouped org-unit UIDs resolve to names via metaData.items.

Generated SQL

Aggregating a data element requires reducing each tracked entity's events for the stage to a single chosen event before joining, otherwise the join would match every event and the aggregation would count events instead of tracked entities.

select t_1."ou",
       avg((ev."eventdatavalues" -> 'UXz7xuGCEhU' ->> 'value')::DECIMAL) as "value"
from analytics_te_nEenWmSyUEp t_1
left join (select trackedentity, eventdatavalues
           from (select trackedentity, eventdatavalues,
                        row_number() over (partition by trackedentity
                                           order by occurreddate desc) as rn
                 from analytics_te_event_nEenWmSyUEp
                 where programstage = 'A03MvHHogjR'
                   and jsonb_exists(eventdatavalues, 'UXz7xuGCEhU')
                   and status != 'SCHEDULE') e
           where rn = 1) ev on ev.trackedentity = t_1.trackedentity
group by t_1."ou"
  • The inner row_number() over (partition by trackedentity order by occurreddate <dir>) + rn = <offset> selects one event per tracked entity (default: the latest event that carries the data element).
  • jsonb_exists(eventdatavalues, '<de>') restricts to events that actually have the value (used instead of the ? operator, which NamedParameterJdbcTemplate would misparse as a bind placeholder).
  • status != 'SCHEDULE' excludes scheduled events (which have no occurreddate), matching the row-level query.
  • The value is aggregated with the per-data-element PostgreSQL cast; COUNT counts non-null values.

@luciano-fiandesio luciano-fiandesio added the run-api-analytics-tests Enables analytics e2e tests label Jul 7, 2026
…tics [DHIS2-21763]

Extend /api/analytics/trackedEntities/aggregate/{tet} so the value request
param can be a program-stage data element (event value), not only a tracked
entity attribute.

Event data-element values are multi-valued per tracked entity (one row per
event in analytics_te_event_<tet>), so a naive join fans out and makes GROUP BY
count events instead of tracked entities. Collapse each TE's events for the
stage to a single chosen row via row_number() and LEFT JOIN it back at TE
grain.
@luciano-fiandesio luciano-fiandesio force-pushed the DHIS2-21763-te-aggregate-event-value-collapse branch from 1db8994 to e0ee63d Compare July 8, 2026 10:10
@sonarqubecloud

sonarqubecloud Bot commented Jul 8, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-api-analytics-tests Enables analytics e2e tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants