Skip to content

Add resource_tables_dirty(timestamp) diagnostic SQL function#1

Closed
jason-p-pickering wants to merge 9 commits into
mainfrom
feature/resource-tables-dirty-check
Closed

Add resource_tables_dirty(timestamp) diagnostic SQL function#1
jason-p-pickering wants to merge 9 commits into
mainfrom
feature/resource-tables-dirty-check

Conversation

@jason-p-pickering

@jason-p-pickering jason-p-pickering commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a standalone, ops-only SQL diagnostic for DHIS2's analytics_rs_* resource tables, as a single
resource_tables_dirty.sql file:

  • resource_tables_dirty(timestamp) RETURNS boolean — cheaply answers "do the resource tables need
    regenerating?" without running a full (expensive) /api/resourceTables build.
  • resource_tables_dirty_detail(timestamp) RETURNS TABLE(resource_table text, reason text) — same
    checks, returns every reason that fired instead of a bare boolean, so a dirty result doesn't require
    guessing which table triggered it.

Each resource table has exactly one _rt_dirty_<name>(baseline) RETURNS TABLE(reason text) probe —
a single source of truth per table. resource_tables_dirty() is just
EXISTS (SELECT 1 FROM resource_tables_dirty_detail(baseline)). (An earlier revision split this
across two files with a parallel boolean checker and TABLE-returning checker per table; every fix
had to be applied twice, so they were merged.)

This is a diagnostic companion to dhis2_analytics_trigger.py, not wired into it yet. Longer-term
intent is to use this as the signal the Python trigger script checks before deciding whether to run
a full resource-table regen — this SQL function is the first step toward that.

Not a Flyway migration, not part of dhis2-core, no Java wiring — deploy manually via
psql -f resource_tables_dirty.sql.

Notable design points, found via live testing on a real 2.41 instance + tracing dhis2-core source

Several tables can be legitimately absent without meaning "stale" — each caused a permanent false
positive (resource_tables_dirty() reading TRUE forever, regardless of baseline) until traced to
its real cause instead of guessed at:

  • analytics_rs_dataapprovalremaplevel / analytics_rs_dataapprovalminlevel: built by a separate
    generateDataApprovalResourceTables() method, never part of a plain /api/resourceTables call, and
    only invoked when isApprovalEnabled() — which traces down to
    keyIgnoreAnalyticsApprovalYearThreshold >= 0 (default -1 = disabled) AND approval levels existing.
    An earlier attempt assumed "usage" meant real dataapproval/dataapprovalworkflowlevels data
    existing — wrong; both "sl" and hmis have genuine approval data yet the tables still don't exist,
    because that data existing isn't the actual DHIS2 gate. Fixed to read the real setting.
  • analytics_rs_dataset and analytics_rs_relationship: both version-gated. Neither exists in
    any DHIS2 2.41.x release — confirmed via GitHub compare against every 2.41.x tag — only from 2.42.0
    onward (dataset: commit 797fd1ed, DHIS2-16705; relationship: commit 7c7fb34e, merged
    2025-02-04). A missing table is only reported dirty when the schema is actually >= 2.42 (detected
    indirectly via flyway_schema_history, since this script has no DHIS2 API/Java dependency).

Test plan

  • Deployed and validated against a local DHIS2 "sl" (Sierra Leone, schema 2.43.64) database.
  • Mutation-tested each staleness type (BEGIN/ROLLBACK, never committed against test data):
    dataset.lastupdated change correctly flags all 3 dependent tables, then rolls back clean.
  • Confirmed each false-positive fix flips to FALSE when the real DHIS2 gate condition isn't
    met, and correctly stays TRUE when there's genuine staleness or a table is missing at a
    schema version where it should exist (e.g. dropping analytics_rs_dataset on this 2.43.64
    schema in a rolled-back transaction still correctly reads dirty).
  • Confirmed clean re-deploy over an already-deployed instance (old boolean-typed functions
    dropped before the new TABLE-returning ones are created, since Postgres refuses to change a
    function's return type via CREATE OR REPLACE).
  • Deployed to hmis (2.41, live production) and confirmed clean baseline (1 genuine finding: indicatorgroupsetstructure staleness, a true positive) plus an end-to-end mutation test — inserting a new period (rolled back, never committed) correctly flipped analytics_rs_periodstructure dirty with reason "period row(s) missing from analytics_rs_periodstructure", confirming the exact scenario that originally motivated this script.

🤖 Generated with Claude Code

…rty; add resource_tables_dirty_detail()

resource_tables_dirty()'s missing-tables guard required all 15 analytics_rs_* tables to exist,
including the 2 that are only created by a full DATA_VALUE build with data approval enabled.
On any instance without that table populated, the guard short-circuited TRUE unconditionally,
forever, regardless of actual staleness. Confirmed on "sl": resource_tables_dirty(now()) read
TRUE even at the current instant.

_rt_dirty_data_approval_remap_level / _rt_dirty_data_approval_min_level now gate on whether
there is actual approval data (dataapprovalworkflowlevels / dataapproval rows) to reflect when
their own resource table is missing, instead of treating the missing table itself as dirty.

Also adds resource_tables_dirty_detail(timestamp), a per-table/per-reason breakdown built by
reusing each checker's exact predicates, so a dirty result no longer requires guessing which of
the 15 tables triggered it.
…version-gate dataset check

Found live on a 2.41 instance ("hmis"): resource_tables_dirty() was permanently TRUE there too,
same bug class as the data-approval fix, but for 2 different tables and 2 different reasons.

- analytics_rs_relationship: usage-gated like the approval tables. Confirmed hmis has 0
  relationship rows (non-tracker instance) -- the resource table plausibly never gets built with
  nothing to reflect. Dirty only if relationship data actually exists.

- analytics_rs_dataset: root-caused via dhis2-core commit 797fd1ed ("fix: Add data set resource
  table [DHIS2-16705]") -- confirmed via GitHub compare that this table does not exist in any
  2.41.x release, only from 2.42.0 onward. Added _rt_dhis2_schema_at_least(), which reads
  flyway_schema_history (dhis2-core's own migration ledger) as an indirect version probe, so a
  missing table is only flagged dirty when the schema is actually >= 2.42 and the table should
  exist. Falls back to not-dirty if the version can't be determined.

Both changes mirrored into resource_tables_dirty_detail.sql. Validated on "sl" (schema 2.43.64):
dropping analytics_rs_dataset in a rolled-back transaction still correctly flags dirty (proving
the version gate doesn't just silently hide real staleness on 2.42+), while real dataset/period
staleness and the data-approval usage-gating from the previous commit remain unaffected.
…und via source tracing

Two things happened together:

1. Merging resource_tables_dirty_detail.sql into resource_tables_dirty.sql. Every fix in this
   script's history so far had a parallel boolean checker and TABLE-returning "detail" checker
   per table, requiring every change to be applied twice -- error-prone, and the actual reason
   this consolidation was requested. Each table now has exactly one _rt_dirty_<name>(baseline)
   RETURNS TABLE(reason text) probe. resource_tables_dirty(baseline) is now just
   `EXISTS (SELECT 1 FROM resource_tables_dirty_detail(baseline))` -- trades away short-circuit
   performance (plpgsql RETURN QUERY materializes fully before a caller's EXISTS can short-circuit)
   for a single source of truth per table, which is the right tradeoff for an ops/cron diagnostic.

2. While rewriting the file, traced analytics_rs_relationship's actual dhis2-core source instead
   of trusting the previous commit's live-observed fix. That fix (usage-gated on `EXISTS(relationship)`)
   was coincidentally right on hmis (0 relationships AND pre-2.42 both say "not dirty") but wrong in
   principle: RelationshipCountResourceTable is an unconditional entry in
   DefaultResourceTableService.getResourceTables() (dhis2-core commit 7c7fb34, merged 2025-02-04),
   confirmed absent from every 2.41.x tag and present from 2.42.0 onward -- the exact same version-gap
   pattern as analytics_rs_dataset, not a real data-existence question. Now version-gated identically
   to analytics_rs_dataset via _rt_dhis2_schema_at_least('2.42').

Re-validated end-to-end on "sl": clean baseline reads FALSE with 0 detail rows, a rolled-back
dataset.lastupdated mutation correctly flags 3 dependent tables, and dropping analytics_rs_dataset
on this 2.43.64 schema still correctly reads dirty (version gate doesn't hide real staleness on
2.42+). Deploying this over an already-deployed 2-file instance requires DROPping the old
boolean-typed functions first (Postgres refuses to change a function's return type via CREATE OR
REPLACE) -- added as a one-time cleanup block at the top of the file.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant