diff --git a/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/engine/retrieve/BaseRetrieveProvider.java b/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/engine/retrieve/BaseRetrieveProvider.java index cab74975b..beff8d92d 100644 --- a/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/engine/retrieve/BaseRetrieveProvider.java +++ b/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/engine/retrieve/BaseRetrieveProvider.java @@ -8,7 +8,6 @@ import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; -import ca.uhn.fhir.rest.param.CompositeParam; import ca.uhn.fhir.rest.param.DateParam; import ca.uhn.fhir.rest.param.InternalCodingDt; import ca.uhn.fhir.rest.param.ParamPrefixEnum; @@ -442,11 +441,13 @@ public void populateDateSearchParams( throw new InternalErrorException("resolved search parameter definition is null"); } - // a date range is a search && condition - so we'll use a composite + // a date range is a search AND condition — each put() on the Multimap + // adds a separate AND clause (one for >= start, one for <= end) DateParam gte = new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, start); DateParam lte = new DateParam(ParamPrefixEnum.LESSTHAN_OR_EQUALS, end); - searchParams.put(sp.getName(), List.of(new CompositeParam<>(gte, lte))); + searchParams.put(sp.getName(), makeMutableSingleElementList(gte)); + searchParams.put(sp.getName(), makeMutableSingleElementList(lte)); } else if (StringUtils.isNotBlank(dateLowPath)) { List dateRangeParam = new ArrayList<>(); DateParam dateParam = new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, start); diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/matcher/ResourceMatcher.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/matcher/ResourceMatcher.java index fa9e7fd5c..e4303fa46 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/matcher/ResourceMatcher.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/matcher/ResourceMatcher.java @@ -262,6 +262,47 @@ default boolean isMatchDate(DateParam param, IBase pathResult) { throw new UnsupportedOperationException( "Expected date, found " + pathResult.getClass().getSimpleName()); } + } else if (pathResult instanceof ICompositeType type) { + // For Period/Timing paths, a single DateParam represents one side of an overlap check. + // This allows repositories that AND repeated date params to evaluate each bound safely. + dateRange = getDateRange(type); + DateParam lowerBound = dateRange.getLowerBound(); + DateParam upperBound = dateRange.getUpperBound(); + + Date resourceStart = lowerBound == null ? null : lowerBound.getValue(); + Date resourceEnd = upperBound == null ? null : upperBound.getValue(); + + if (param.getValue() == null) { + return false; + } + + switch (param.getPrefix()) { + case GREATERTHAN: + case GREATERTHAN_OR_EQUALS: + return resourceEnd != null && isDateMatch(param, resourceEnd); + case LESSTHAN: + case LESSTHAN_OR_EQUALS: + return resourceStart != null && isDateMatch(param, resourceStart); + case EQUAL: + if (resourceStart == null || resourceEnd == null) { + return false; + } + + Date compareDate = param.getValue(); + return !compareDate.before(resourceStart) && !compareDate.after(resourceEnd); + case NOT_EQUAL: + if (resourceStart == null || resourceEnd == null) { + return true; + } + + Date notEqualCompare = param.getValue(); + return notEqualCompare.before(resourceStart) || notEqualCompare.after(resourceEnd); + default: + String msg = String.format( + "Unsupported DateTime comparison operation %s", + param.getPrefix().getValue()); + throw new UnsupportedOperationException(msg); + } } else { throw new UnsupportedOperationException( "Expected element of type date, dateTime, instant, Timing or Period, found " diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/matcher/ResourceMatcherTest.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/matcher/ResourceMatcherTest.java index 9bbf86ec5..f2779ec09 100644 --- a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/matcher/ResourceMatcherTest.java +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/matcher/ResourceMatcherTest.java @@ -59,6 +59,38 @@ void before() { resourceMatcher = new ResourceMatcherR4(); } + @Test + void matches_locationPeriodWithSingleDateParam_doesNotThrowAndRespectsBounds() { + var encounter = new Encounter(); + encounter + .addLocation() + .setPeriod(new Period() + .setStart(createDate("2000-01-01 00:00:00")) + .setEnd(createDate("2000-12-31 23:59:59"))); + + assertTrue(resourceMatcher.matches( + "location-period", + List.of(new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, createDate("2000-01-01"))), + encounter)); + assertTrue(resourceMatcher.matches( + "location-period", + List.of(new DateParam(ParamPrefixEnum.LESSTHAN_OR_EQUALS, createDate("2000-12-31"))), + encounter)); + + assertEquals( + false, + resourceMatcher.matches( + "location-period", + List.of(new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, createDate("2001-01-01"))), + encounter)); + assertEquals( + false, + resourceMatcher.matches( + "location-period", + List.of(new DateParam(ParamPrefixEnum.LESSTHAN_OR_EQUALS, createDate("1999-12-31"))), + encounter)); + } + // NB: the list of parameters are always OR'd // internal compositeparams are always AND'd static List coverageParameters() {