From 38dfaf5f64f4e12123e810fccc2da356a0fc70f8 Mon Sep 17 00:00:00 2001 From: Dhruv Gupta Date: Sat, 24 Oct 2026 06:25:19 -0400 Subject: [PATCH] Include Type Concept vocabulary in Data Sources condition by type report The condition "by type" drilldown queries only join condition type concepts from the 'Condition Type' vocabulary. That vocabulary was deprecated in 2020 when the type concepts were consolidated into the 'Type Concept' vocabulary, so CDMs that use current condition_type_concept_id values (for example 32817 EHR or 32810 Claim) get an empty "Condition by type" chart in Data Sources. Also accept concepts from the 'Type Concept' vocabulary and apply the inpatient/outpatient primary/secondary grouping only to the old 'Condition Type' concepts it was written for, so new type concepts are shown under their own names. Adds a test that runs the condition drilldown and drilldown summary queries against the embedded Postgres database with both old and new condition type concepts. Fixes OHDSI/Atlas#2644 --- .../sql/report/condition/drilldown/byType.sql | 10 +- .../condition/drilldownsummary/byType.sql | 10 +- .../report/CDMResultsConditionByTypeTest.java | 133 ++++++++++++++++++ 3 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 src/test/java/org/ohdsi/webapi/report/CDMResultsConditionByTypeTest.java diff --git a/src/main/resources/resources/cdmresults/sql/report/condition/drilldown/byType.sql b/src/main/resources/resources/cdmresults/sql/report/condition/drilldown/byType.sql index 5f0e180bfd..bfded95720 100644 --- a/src/main/resources/resources/cdmresults/sql/report/condition/drilldown/byType.sql +++ b/src/main/resources/resources/cdmresults/sql/report/condition/drilldown/byType.sql @@ -11,13 +11,16 @@ ON ar1.stratum_1 = CAST(c1.concept_id AS VARCHAR(255)) INNER JOIN ( SELECT concept_id, +CASE WHEN lower(vocabulary_id) = 'condition type' THEN CASE WHEN concept_name LIKE 'Inpatient%' THEN 10 WHEN concept_name LIKE 'Outpatient%' THEN 20 ELSE concept_id END + CASE WHEN (concept_name LIKE 'Inpatient%' OR concept_name LIKE 'Outpatient%' ) AND (concept_name LIKE '%primary%' OR concept_name LIKE '%1st position%') THEN 1 WHEN (concept_name LIKE 'Inpatient%' OR concept_name LIKE 'Outpatient%' ) AND (concept_name NOT LIKE '%primary%' AND concept_name NOT LIKE '%1st position%') THEN 2 -ELSE 0 END AS concept_group_id, +ELSE 0 END +ELSE concept_id END AS concept_group_id, +CASE WHEN lower(vocabulary_id) = 'condition type' THEN CONCAT( CASE WHEN concept_name LIKE 'Inpatient%' THEN 'Claim- Inpatient: ' WHEN concept_name LIKE 'Outpatient%' THEN 'Claim- Outpatient: ' @@ -25,9 +28,10 @@ CONCAT( CASE WHEN (concept_name LIKE 'Inpatient%' OR concept_name LIKE 'Outpatient%' ) AND (concept_name LIKE '%primary%' OR concept_name LIKE '%1st position%') THEN 'Primary diagnosis' WHEN (concept_name LIKE 'Inpatient%' OR concept_name LIKE 'Outpatient%' ) AND (concept_name NOT LIKE '%primary%' AND concept_name NOT LIKE '%1st position%') THEN 'Secondary diagnosis' ELSE '' END -) AS concept_group_name +) +ELSE concept_name END AS concept_group_name FROM @vocab_database_schema.concept -WHERE lower(vocabulary_id) = 'condition type' +WHERE lower(vocabulary_id) IN ('condition type', 'type concept') ) c2 ON ar1.stratum_2 = CAST(c2.concept_id AS VARCHAR(255)) diff --git a/src/main/resources/resources/cdmresults/sql/report/condition/drilldownsummary/byType.sql b/src/main/resources/resources/cdmresults/sql/report/condition/drilldownsummary/byType.sql index 38727cfffa..646fae26b1 100644 --- a/src/main/resources/resources/cdmresults/sql/report/condition/drilldownsummary/byType.sql +++ b/src/main/resources/resources/cdmresults/sql/report/condition/drilldownsummary/byType.sql @@ -11,13 +11,16 @@ ON ar1.stratum_1 = CAST(c1.concept_id AS VARCHAR(255)) INNER JOIN ( SELECT concept_id, +CASE WHEN lower(vocabulary_id) = 'condition type' THEN CASE WHEN concept_name LIKE 'Inpatient%' THEN 10 WHEN concept_name LIKE 'Outpatient%' THEN 20 ELSE concept_id END + CASE WHEN (concept_name LIKE 'Inpatient%' OR concept_name LIKE 'Outpatient%' ) AND (concept_name LIKE '%primary%' OR concept_name LIKE '%1st position%') THEN 1 WHEN (concept_name LIKE 'Inpatient%' OR concept_name LIKE 'Outpatient%' ) AND (concept_name NOT LIKE '%primary%' AND concept_name NOT LIKE '%1st position%') THEN 2 -ELSE 0 END AS concept_group_id, +ELSE 0 END +ELSE concept_id END AS concept_group_id, +CASE WHEN lower(vocabulary_id) = 'condition type' THEN CONCAT( CASE WHEN concept_name LIKE 'Inpatient%' THEN 'Claim- Inpatient: ' WHEN concept_name LIKE 'Outpatient%' THEN 'Claim- Outpatient: ' @@ -25,9 +28,10 @@ CONCAT( CASE WHEN (concept_name LIKE 'Inpatient%' OR concept_name LIKE 'Outpatient%' ) AND (concept_name LIKE '%primary%' OR concept_name LIKE '%1st position%') THEN 'Primary diagnosis' WHEN (concept_name LIKE 'Inpatient%' OR concept_name LIKE 'Outpatient%' ) AND (concept_name NOT LIKE '%primary%' AND concept_name NOT LIKE '%1st position%') THEN 'Secondary diagnosis' ELSE '' END -) AS concept_group_name +) +ELSE concept_name END AS concept_group_name FROM @vocab_database_schema.concept -WHERE lower(vocabulary_id) = 'condition type' +WHERE lower(vocabulary_id) IN ('condition type', 'type concept') ) c2 ON ar1.stratum_2 = CAST(c2.concept_id AS VARCHAR(255)) diff --git a/src/test/java/org/ohdsi/webapi/report/CDMResultsConditionByTypeTest.java b/src/test/java/org/ohdsi/webapi/report/CDMResultsConditionByTypeTest.java new file mode 100644 index 0000000000..9df4dcde90 --- /dev/null +++ b/src/test/java/org/ohdsi/webapi/report/CDMResultsConditionByTypeTest.java @@ -0,0 +1,133 @@ +package org.ohdsi.webapi.report; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.HashMap; +import java.util.Map; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.ohdsi.webapi.AbstractDatabaseTest; +import org.ohdsi.webapi.source.Source; +import org.ohdsi.webapi.source.SourceDaimon; + +/** + * Runs the Data Sources condition drilldown queries against the embedded Postgres instance and checks + * the "by type" results for condition type concepts from both the deprecated 'Condition Type' vocabulary + * and the current 'Type Concept' vocabulary. + */ +public class CDMResultsConditionByTypeTest extends AbstractDatabaseTest { + + private static final String RESULTS_SCHEMA = "condition_by_type_results"; + + private static final int CONDITION_CONCEPT_ID = 201826; + + private CDMResultsAnalysisRunner runner; + private Source source; + + @Before + public void setUp() { + + runner = new CDMResultsAnalysisRunner(); + runner.init("postgresql", new ObjectMapper()); + + source = mock(Source.class); + when(source.getTableQualifier(SourceDaimon.DaimonType.Results)).thenReturn(RESULTS_SCHEMA); + when(source.getTableQualifier(SourceDaimon.DaimonType.Vocabulary)).thenReturn(RESULTS_SCHEMA); + when(source.getTableQualifier(SourceDaimon.DaimonType.CDM)).thenReturn(RESULTS_SCHEMA); + when(source.getSourceDialect()).thenReturn("postgresql"); + + jdbcTemplate.execute(String.format("DROP SCHEMA IF EXISTS %1$s CASCADE; CREATE SCHEMA %1$s;", RESULTS_SCHEMA)); + jdbcTemplate.execute(String.format("CREATE TABLE %s.achilles_results (analysis_id INT, stratum_1 VARCHAR(255), " + + "stratum_2 VARCHAR(255), stratum_3 VARCHAR(255), stratum_4 VARCHAR(255), stratum_5 VARCHAR(255), " + + "count_value BIGINT)", RESULTS_SCHEMA)); + jdbcTemplate.execute(String.format("CREATE TABLE %s.achilles_results_dist (analysis_id INT, " + + "stratum_1 VARCHAR(255), stratum_2 VARCHAR(255), stratum_3 VARCHAR(255), stratum_4 VARCHAR(255), " + + "stratum_5 VARCHAR(255), count_value BIGINT, min_value FLOAT, max_value FLOAT, avg_value FLOAT, " + + "stdev_value FLOAT, median_value FLOAT, p10_value FLOAT, p25_value FLOAT, p75_value FLOAT, " + + "p90_value FLOAT)", RESULTS_SCHEMA)); + jdbcTemplate.execute(String.format("CREATE TABLE %s.concept (concept_id INT, concept_name VARCHAR(255), " + + "domain_id VARCHAR(20), vocabulary_id VARCHAR(20), concept_class_id VARCHAR(20), " + + "standard_concept VARCHAR(1), concept_code VARCHAR(50))", RESULTS_SCHEMA)); + + insertConcept(CONDITION_CONCEPT_ID, "Type 2 diabetes mellitus", "Condition", "SNOMED", "Clinical Finding", "S"); + // condition type concepts from the vocabulary that was deprecated in 2020 + insertConcept(38000200, "Inpatient header - 1st position", "Type Concept", "Condition Type", "Condition Type", null); + insertConcept(38000201, "Inpatient header - 2nd position", "Type Concept", "Condition Type", "Condition Type", null); + // condition type concepts from the current 'Type Concept' vocabulary + insertConcept(32817, "EHR", "Type Concept", "Type Concept", "Type Concept", "S"); + insertConcept(32810, "Claim", "Type Concept", "Type Concept", "Type Concept", "S"); + insertConcept(32855, "Inpatient claim header", "Type Concept", "Type Concept", "Type Concept", "S"); + + // number of condition occurrence records by condition_concept_id and condition_type_concept_id + insertResult(405, CONDITION_CONCEPT_ID, 38000200, 5); + insertResult(405, CONDITION_CONCEPT_ID, 38000201, 3); + insertResult(405, CONDITION_CONCEPT_ID, 32817, 7); + insertResult(405, CONDITION_CONCEPT_ID, 32810, 11); + insertResult(405, CONDITION_CONCEPT_ID, 32855, 13); + } + + @After + public void tearDown() { + + jdbcTemplate.execute(String.format("DROP SCHEMA IF EXISTS %s CASCADE", RESULTS_SCHEMA)); + } + + private void insertConcept(int conceptId, String conceptName, String domainId, String vocabularyId, + String conceptClassId, String standardConcept) { + + jdbcTemplate.update(String.format("INSERT INTO %s.concept (concept_id, concept_name, domain_id, vocabulary_id, " + + "concept_class_id, standard_concept, concept_code) VALUES (?, ?, ?, ?, ?, ?, ?)", RESULTS_SCHEMA), + conceptId, conceptName, domainId, vocabularyId, conceptClassId, standardConcept, + String.valueOf(conceptId)); + } + + private void insertResult(int analysisId, int stratum1, int stratum2, long countValue) { + + jdbcTemplate.update(String.format("INSERT INTO %s.achilles_results (analysis_id, stratum_1, stratum_2, " + + "count_value) VALUES (?, ?, ?, ?)", RESULTS_SCHEMA), + analysisId, String.valueOf(stratum1), String.valueOf(stratum2), countValue); + } + + private static Map countsByTypeName(JsonNode byType) { + + Map counts = new HashMap<>(); + for (JsonNode row : byType) { + assertEquals(CONDITION_CONCEPT_ID, row.get("conditionConceptId").asInt()); + counts.put(row.get("conceptName").asText(), row.get("countValue").asLong()); + } + return counts; + } + + private static Map expectedCounts() { + + Map expected = new HashMap<>(); + expected.put("Claim- Inpatient: Primary diagnosis", 5L); + expected.put("Claim- Inpatient: Secondary diagnosis", 3L); + expected.put("EHR", 7L); + expected.put("Claim", 11L); + // the inpatient/outpatient grouping only applies to the deprecated 'Condition Type' concepts + expected.put("Inpatient claim header", 13L); + return expected; + } + + @Test + public void getDrilldown_byTypeIncludesTypeConceptVocabulary() { + + JsonNode drilldown = runner.getDrilldown(jdbcTemplate, "condition", CONDITION_CONCEPT_ID, source); + + assertEquals(expectedCounts(), countsByTypeName(drilldown.get("byType"))); + } + + @Test + public void getDrilldownSummary_byTypeIncludesTypeConceptVocabulary() { + + JsonNode drilldown = runner.getDrilldown(jdbcTemplate, "condition", null, source); + + assertEquals(expectedCounts(), countsByTypeName(drilldown.get("byType"))); + } +}