Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 232 additions & 0 deletions PRPs/prp-population-basis-primitive-duplicate-counting.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,11 @@ public boolean add(T newElement) {
}
}

final CqlType newElementCqlType = FhirResourceAndCqlTypeUtils.castToCqlTypeIfApplicable(newElement);

if (newElementCqlType != null) {
if (this.contains(newElementCqlType)) {
return false;
} else {
return super.add(newElement);
}
}
// CDO-714 tactical fix: the CqlType dedup branch is disabled so distinct runtime.Date
// (and other no-equals-override CqlType) instances survive default HashSet identity
// semantics. The holistic, basis-aware design is captured in
// PRPs/prp-population-basis-primitive-duplicate-counting.md and is sequenced after the
// CQL 5.0 (cql1) ExpressionResult type changes land.

return super.add(newElement);
}
Expand Down Expand Up @@ -116,17 +112,8 @@ public boolean remove(Object removalCandidate) {
return false;
}

final CqlType removalCandidateCqlType = FhirResourceAndCqlTypeUtils.castToCqlTypeIfApplicable(removalCandidate);

if (removalCandidateCqlType != null) {
for (T next : this) {
if (next instanceof CqlType nextCqlType
&& FhirResourceAndCqlTypeUtils.areEqualCqlTypes(nextCqlType, removalCandidateCqlType)) {
return super.remove(nextCqlType);
}
}
return false;
}
// CDO-714 tactical fix: matching CqlType remove path disabled. See add() above and
// PRPs/prp-population-basis-primitive-duplicate-counting.md.

return super.remove(removalCandidate);
}
Expand Down Expand Up @@ -165,10 +152,11 @@ public boolean retainAll(@Nonnull Collection<?> otherCollection) {

private static boolean contains(Collection<?> collection, Object obj) {
final IBaseResource otherResource = FhirResourceAndCqlTypeUtils.castToResourceIfApplicable(obj);
final CqlType otherCqlType = FhirResourceAndCqlTypeUtils.castToCqlTypeIfApplicable(obj);
// CDO-714 tactical fix: CqlType detection disabled here too. See add() above and
// PRPs/prp-population-basis-primitive-duplicate-counting.md.

// prevent infinite recursion
if (otherResource != null || otherCqlType != null || collection instanceof HashSetForFhirResourcesAndCqlTypes) {
if (otherResource != null || collection instanceof HashSetForFhirResourcesAndCqlTypes) {
return containsInner(collection, obj);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.opencds.cqf.cql.engine.runtime.Date;
import org.opencds.cqf.cql.engine.runtime.Precision;
Expand Down Expand Up @@ -56,6 +57,7 @@ void removeFhirResourceByIdRemovesCorrectResource() {
assertFalse(set.contains(patient2));
}

@Disabled("CDO-714 — CqlType dedup branch disabled; see PRPs/prp-population-basis-primitive-duplicate-counting.md")
@Test
void removeCqlDateRemovesCorrectCqlDate() {
var set = new HashSetForFhirResourcesAndCqlTypes<Date>();
Expand Down Expand Up @@ -88,6 +90,7 @@ void retainAllKeepsOnlyMatchingFhirResourcesById() {
assertEquals(1, set.size());
}

@Disabled("CDO-714 — CqlType dedup branch disabled; see PRPs/prp-population-basis-primitive-duplicate-counting.md")
@Test
void retainAllKeepsOnlyMatchingCqlDate() {
var set = new HashSetForFhirResourcesAndCqlTypes<Date>();
Expand All @@ -104,6 +107,7 @@ void retainAllKeepsOnlyMatchingCqlDate() {
assertEquals(1, set.size());
}

@Disabled("CDO-714 — CqlType dedup branch disabled; see PRPs/prp-population-basis-primitive-duplicate-counting.md")
@Test
void retainAllKeepsOnlyMatchingCqlDateWithMatchingPrecision() {
var set = new HashSetForFhirResourcesAndCqlTypes<Date>();
Expand All @@ -120,6 +124,7 @@ void retainAllKeepsOnlyMatchingCqlDateWithMatchingPrecision() {
assertEquals(1, set.size());
}

@Disabled("CDO-714 — CqlType dedup branch disabled; see PRPs/prp-population-basis-primitive-duplicate-counting.md")
@Test
void retainAllKeepsOnlyMatchingCqlDateWithPrecisionMismatch() {
var set = new HashSetForFhirResourcesAndCqlTypes<Date>();
Expand Down Expand Up @@ -153,6 +158,7 @@ void removeAllRemovesMatchingFhirResourcesById() {
assertEquals(1, set.size());
}

@Disabled("CDO-714 — CqlType dedup branch disabled; see PRPs/prp-population-basis-primitive-duplicate-counting.md")
@Test
void removeAllRemovesMatchingCqlDate() {
var set = new HashSetForFhirResourcesAndCqlTypes<Date>();
Expand All @@ -169,6 +175,7 @@ void removeAllRemovesMatchingCqlDate() {
assertEquals(1, set.size());
}

@Disabled("CDO-714 — CqlType dedup branch disabled; see PRPs/prp-population-basis-primitive-duplicate-counting.md")
@Test
void removeAllRemovesMatchingCqlDateWithPrecision() {
var set = new HashSetForFhirResourcesAndCqlTypes<Date>();
Expand All @@ -185,6 +192,7 @@ void removeAllRemovesMatchingCqlDateWithPrecision() {
assertEquals(1, set.size());
}

@Disabled("CDO-714 — CqlType dedup branch disabled; see PRPs/prp-population-basis-primitive-duplicate-counting.md")
@Test
void removeAllRemovesMatchingCqlDateMismatchPrecision() {
var set = new HashSetForFhirResourcesAndCqlTypes<Date>();
Expand Down Expand Up @@ -218,6 +226,7 @@ void addAllAddsOnlyNonDuplicateFhirResourcesById() {
assertEquals(2, set.size());
}

@Disabled("CDO-714 — CqlType dedup branch disabled; see PRPs/prp-population-basis-primitive-duplicate-counting.md")
@Test
void addAllAddsNoDuplicateCqlDates() {
var set = new HashSetForFhirResourcesAndCqlTypes<Date>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package org.opencds.cqf.fhir.cr.measure.r4;

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType;
import org.opencds.cqf.fhir.cr.measure.r4.Measure.Given;

/**
* Integration test pinning the expected behaviour for CDO-714.
*
* <p>When a Measure has a non-boolean population basis and a CQL expression for a population
* yields duplicate values within a single subject, each occurrence is a discrete data point and
* the population count must include the duplicates.
*
* <p>This test covers three flavours of "primitive" basis that the evaluator must handle:
* <ul>
* <li><b>CQL Date</b> ({@code System.Date} → {@code runtime.Date}) yielded by a literal list
* expression. The tactical CDO-714 fix disables the {@code CqlType} dedup branches in
* {@code HashSetForFhirResourcesAndCqlTypes} so distinct {@code runtime.Date} instances
* survive default {@link java.util.HashSet} identity semantics and the count reflects every
* yielded date.
* <li><b>CQL Integer</b> ({@code System.Integer} → {@code java.lang.Integer}) yielded by a
* literal list expression. <b>Deferred to the holistic fix</b>: {@code Integer} is neither
* {@code IBaseResource} nor {@code CqlType}, and {@code Integer}'s value-based
* {@code equals}/{@code hashCode} still dedups in the default {@link java.util.HashSet}
* path. See {@code PRPs/prp-population-basis-primitive-duplicate-counting.md}.
* <li><b>FHIR primitive</b> ({@link org.hl7.fhir.instance.model.api.IPrimitiveType}) yielded by
* walking {@code Patient.address[0].line}. Regression guard: HAPI FHIR primitive instances
* are neither {@code IBaseResource} nor {@code CqlType}, and {@code Base.equals} is not
* overridden, so default {@link java.util.HashSet} identity semantics keep distinct
* instances. The tactical fix must (and does) preserve this.
* </ul>
*
* @see <a href="https://simpaticois.atlassian.net/browse/CDO-714">CDO-714</a>
*/
@SuppressWarnings("squid:S2699")
class DuplicateTypeIntraSubjectTest {

private static final Given GIVEN = Measure.given().repositoryFor("DuplicateTypeIntraSubject");

@Test
void cqlDateBasis_intraSubjectDuplicates_populationReport_includesDuplicates() {
GIVEN.when()
.measureId("DuplicateTypeIntraSubjectDateBasisMeasure")
.periodStart("2025-01-01")
.periodEnd("2025-12-31")
.reportType("population")
.evaluate()
.then()
.firstGroup()
.population(MeasurePopulationType.INITIALPOPULATION)
.hasCount(3)
.up()
.up()
.report();
}

@Test
void cqlDateBasis_intraSubjectDuplicates_subjectReport_includesDuplicates() {
GIVEN.when()
.measureId("DuplicateTypeIntraSubjectDateBasisMeasure")
.subject("Patient/patient-a")
.periodStart("2025-01-01")
.periodEnd("2025-12-31")
.evaluate()
.then()
.firstGroup()
.population(MeasurePopulationType.INITIALPOPULATION)
.hasCount(3)
.up()
.up()
.report();
}

// Deferred: the CQL engine returns java.lang.Integer for CQL Integer literals, which has
// value-based equals/hashCode and dedups via default HashSet semantics. The tactical
// CDO-714 fix (disabling CqlType branches in HashSetForFhirResourcesAndCqlTypes) does not
// reach the java.lang.Integer path. The holistic fix is captured in
// PRPs/prp-population-basis-primitive-duplicate-counting.md and is sequenced after the
// CQL 5.0 (cql1) ExpressionResult type changes land.
@Disabled("CDO-714 — deferred to PRPs/prp-population-basis-primitive-duplicate-counting.md")
@Test
void cqlIntegerBasis_intraSubjectDuplicates_populationReport_includesDuplicates() {
GIVEN.when()
.measureId("DuplicateTypeIntraSubjectIntegerBasisMeasure")
.periodStart("2025-01-01")
.periodEnd("2025-12-31")
.reportType("population")
.evaluate()
.then()
.firstGroup()
.population(MeasurePopulationType.INITIALPOPULATION)
.hasCount(3)
.up()
.up()
.report();
}

@Test
void fhirStringBasis_intraSubjectDuplicates_populationReport_includesDuplicates() {
GIVEN.when()
.measureId("DuplicateTypeIntraSubjectFhirStringBasisMeasure")
.periodStart("2025-01-01")
.periodEnd("2025-12-31")
.reportType("population")
.evaluate()
.then()
.firstGroup()
.population(MeasurePopulationType.INITIALPOPULATION)
.hasCount(3)
.up()
.up()
.report();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@
import org.opencds.cqf.fhir.utility.BundleHelper;
import org.opencds.cqf.fhir.utility.repository.ig.IgRepository;
import org.opencds.cqf.fhir.utility.search.Searches.SearchBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@SuppressWarnings("squid:S1135")
class MultiMeasure {
Expand Down Expand Up @@ -475,6 +477,10 @@ public MeasureReport resourceToMeasureReport(
}

public static class SelectedMeasureReport extends MultiMeasure.Selected<MeasureReport, SelectedReport> {
private static final Logger logger = LoggerFactory.getLogger(SelectedMeasureReport.class);
private static final IParser JSON_PARSER =
FhirContext.forR4Cached().newJsonParser().setPrettyPrint(true);

public MeasureReport report() {
return this.value();
}
Expand Down Expand Up @@ -627,6 +633,13 @@ public SelectedMeasureReport hasPeriodEnd(Date periodEnd) {
assertEquals(periodEnd, period.getEnd());
return this;
}

// Log the JSON corresponding to the report at this point:
public SelectedMeasureReport logReportJson() {
logger.info(JSON_PARSER.encodeResourceToString(report()));

return this;
}
}

public static class SelectedGroup
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package org.opencds.cqf.fhir.cr.measure.r4;

import org.junit.jupiter.api.Test;
import org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType;
import org.opencds.cqf.fhir.cr.measure.r4.MultiMeasure.Given;

/**
* MultiMeasure integration test pinning CDO-714 behaviour across the {@link R4MultiMeasureService}
* orchestration path.
*
* <p>Runs three Measures in the same evaluation call against a single patient: a CQL-Date-basis
* Measure with intra-subject duplicate dates, a FHIR-string-basis Measure with intra-subject
* duplicate {@link org.hl7.fhir.instance.model.api.IPrimitiveType} strings, and a boolean-basis
* Measure used as an unaffected baseline.
*
* <p>The date-basis Measure reports a population count of {@code 3} (each duplicate counted) under
* the tactical CDO-714 fix; the FHIR-string-basis Measure also reports {@code 3}; and the
* boolean-basis Measure stays at {@code 1}.
*
* <p>The CQL-Integer-basis Measure is deliberately excluded from this chain — the tactical fix
* does not reach the {@code java.lang.Integer} dedup path. The full multi-basis assertion
* (date / integer / FHIR string / boolean) will be restored when the holistic fix lands; see
* {@code PRPs/prp-population-basis-primitive-duplicate-counting.md}.
*
* @see <a href="https://simpaticois.atlassian.net/browse/CDO-714">CDO-714</a>
*/
@SuppressWarnings("squid:S2699")
class MultiMeasureDuplicateTypeIntraSubjectTest {

private static final Given GIVEN = MultiMeasure.given().repositoryFor("DuplicateTypeIntraSubject");

@Test
void multiMeasure_dateBasis_fhirStringBasis_booleanBasis_countsAreBasisAware() {
// CQL-Integer-basis Measure intentionally omitted from this chain; see class JavaDoc and
// PRPs/prp-population-basis-primitive-duplicate-counting.md for the deferred coverage.
var when = GIVEN.when()
.measureId("DuplicateTypeIntraSubjectDateBasisMeasure")
.measureId("DuplicateTypeIntraSubjectFhirStringBasisMeasure")
.measureId("DuplicateTypeIntraSubjectBooleanBasisMeasure")
.periodStart("2025-01-01")
.periodEnd("2025-12-31")
.reportType("population")
.evaluate();

when.then()
.hasBundleCount(1)
.hasMeasureReportCount(3)
.measureReport("http://example.com/Measure/DuplicateTypeIntraSubjectDateBasisMeasure")
.firstGroup()
.population(MeasurePopulationType.INITIALPOPULATION)
.hasCount(3)
.up()
.up()
.up()
.measureReport("http://example.com/Measure/DuplicateTypeIntraSubjectFhirStringBasisMeasure")
.firstGroup()
.population(MeasurePopulationType.INITIALPOPULATION)
.hasCount(3)
.up()
.up()
.up()
.measureReport("http://example.com/Measure/DuplicateTypeIntraSubjectBooleanBasisMeasure")
.firstGroup()
.population(MeasurePopulationType.INITIALPOPULATION)
.hasCount(1)
.up()
.up()
.up()
.report();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
library DuplicateTypeIntraSubject

using FHIR version '4.0.1'

include FHIRHelpers version '4.0.1' called FHIRHelpers

context Patient

// CDO-714: intra-subject duplicate values for a non-boolean population basis.
//
// Case 1: CQL primitive type (System.Date).
// Expression yields three CQL Date values with @2025-07-03 appearing twice.
// Expected count = 3, actual count today = 2 (CqlType dedup path collapses them).
define "Date Initial Population":
{ @2025-07-03, @2025-07-03, @2025-07-04 }

// Case 2: FHIR primitive type (IPrimitiveType<String> via Patient.address.line).
// The patient fixture has address[0].line = ["dup-line", "dup-line", "other-line"]
// so the expression yields three FHIR string primitives, with "dup-line" twice.
// Expected count = 3.
define "Fhir String Initial Population":
Patient.address[0].line

// Case 3: CQL primitive type (System.Integer, represented as java.lang.Integer).
// Expression yields three integers with 42 appearing twice.
// java.lang.Integer is neither IBaseResource nor CqlType, so it falls through to default
// HashSet semantics, and Integer's value-based equals/hashCode dedups the two 42s.
// Expected count = 3, actual count today = 2.
define "Integer Initial Population":
{ 42, 42, 43 }

// Boolean basis comparison: one patient => count = 1, unchanged by the fix.
define "Boolean Initial Population":
true
Loading
Loading