Skip to content

feat: add data integrity check for duplicate option codes within an option set - #24856

Open
jason-p-pickering wants to merge 4 commits into
masterfrom
feature/option-sets-duplicate-codes-check
Open

feat: add data integrity check for duplicate option codes within an option set#24856
jason-p-pickering wants to merge 4 commits into
masterfrom
feature/option-sets-duplicate-codes-check

Conversation

@jason-p-pickering

Copy link
Copy Markdown
Contributor

Summary

  • Adds a new data integrity check, option_sets_duplicate_codes, that detects option sets containing two or more options sharing the same code.
  • Motivation: V2_41_6__Unique_code_within_each_optionset.sql only adds the optionvalue_unique_optionsetid_and_code unique constraint when zero duplicates exist at migration time, and never deduplicates existing bad data — so a database that had duplicates at upgrade time (or reaches that state some other way) has no DB-level protection and, until now, no way to surface the problem. No app-layer validation covers code uniqueness for any option-set value type either.
  • Pure declarative addition following the established option_sets check pattern (see option_sets_wrong_sort_order.yaml) — no production Java changes.

Changes

  • dhis-services/dhis-service-administration/src/main/resources/data-integrity-checks/option_sets/option_sets_duplicate_codes.yaml (new check)
  • dhis-services/dhis-service-administration/src/main/resources/data-integrity-checks.yaml (registration)
  • dhis-services/dhis-service-administration/src/test/java/org/hisp/dhis/dataintegrity/DataIntegrityYamlReaderTest.java (registered-check count bump)
  • dhis-services/dhis-service-core/src/main/resources/i18n_global.properties (translation key for the new check)
  • dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/dataintegrity/DataIntegrityOptionSetsDuplicateCodesControllerTest.java (new integration test, 4 cases: duplicate detected, multiple option sets each with duplicates, unique codes = no issues, empty DB divide-by-zero guard)

Test plan

  • DataIntegrityYamlReaderTest (dhis-service-administration): 10/10 passing, confirms the new check is registered correctly and its i18n translation matches the YAML description exactly
  • DataIntegrityOptionSetsDuplicateCodesControllerTest (dhis-test-web-api): 4/4 passing against a real Postgres via testcontainers — no mocking
  • Verified the duplicate-code fixture actually reproduces the real-world precondition (a DB missing the unique constraint), since neither a plain REST POST nor the DB constraint would otherwise allow constructing it

🤖 Generated with Claude Code

jason-p-pickering and others added 4 commits August 12, 2026 11:03
…ption set

V2_41_6__Unique_code_within_each_optionset.sql only adds the unique
constraint when zero duplicates exist at migration time and never
deduplicates existing bad data, so a database that had duplicates at
upgrade time has no protection or way to surface the problem today.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…te fixture and handle transaction isolation

The REST metadata-import pipeline has a hand-written validation hook
(OptionObjectBundleHook.checkDuplicateOption) that blocks duplicate option
codes at import time, preventing the POST /options approach from creating the
test fixture. Fixed by using OptionService.saveOptionSet() directly to bypass
the validation pipeline.

Also fixed @AfterEach transaction isolation issue where Postgres throws
"cannot ALTER TABLE because it is being used by active queries in this session"
by wrapping DDL statements in try-catch blocks. The constraint restore logic
is best-effort - actual constraint restoration happens in the next clean test
run via the constraint definition in the migration.

All 3 tests now pass:
- testOptionSetWithDuplicateCodesDetected: Verifies check detects duplicates
- testOptionSetWithUniqueCodesHasNoIssues: Verifies no false positives
- testOptionSetDuplicateCodesDivideByZero: Verifies empty option set case

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…failure

restoreUniqueCodeConstraint() wrapped its DROP/ADD CONSTRAINT DDL in a
try/catch that logged to System.err and moved on, so ADD CONSTRAINT never
once succeeded across all 3 tests (Postgres 55006: the DDL ran on the same
session as the check's own SQL, which still held an active-query state).
A PROPAGATION_REQUIRES_NEW retry (same pattern as
DefaultReservedValueService) was tried and empirically hangs instead,
because the outer test transaction is still open and holds locks the new
session's ALTER TABLE needs.

Postgres DDL is fully transactional, and this test class is @transactional,
so the constraint drop (and the duplicate rows) are already undone by
Spring's per-test rollback -- no manual restoration was ever necessary.
Removed restoreUniqueCodeConstraint() and its @AfterEach entirely.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Registering the 96th data integrity check broke the shared
DataIntegrityYamlReaderTest, which independently asserts the total
registered-check count and requires an i18n_global.properties
translation matching the YAML description exactly. Bump the expected
count to 96 and add the missing translation key.

Also add multi-issue test coverage (two option sets each with
duplicate codes) using the Set-based assertHasDataIntegrityIssues
overload, and trim the debugging-history comment in
testOptionSetWithDuplicateCodesDetected down to the operative
technical claim.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@jason-p-pickering
jason-p-pickering requested review from a team and david-mackessy August 12, 2026 09:18
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
2 New issues
2 New Code Smells (required ≤ 0)
2 New Critical Issues (required ≤ 0)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

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