LastUpdated search doesn't work with HFQL (#5510)

* LastUpdated search doesn't work with HFQL

* Spotless
This commit is contained in:
James Agnew 2023-12-14 17:24:04 -05:00 committed by GitHub
parent c4ac940e14
commit d187399ce5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 496 additions and 160 deletions

View File

@ -0,0 +1,7 @@
---
type: fix
jira: SMILE-7664
title: "The HFQL/SQL engine incorrectly parsed expressions containing a `>=` or
`<=` comparator in a WHERE clause. This has been corrected. Additionally, the
execution engine has been optimized to apply clauses against the `meta.lastUpdated`
path more efficiently by using the equivalent search parameter automatically."

View File

@ -42,6 +42,7 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.DateOrListParam;
import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.ParamPrefixEnum;
import ca.uhn.fhir.rest.param.ParameterUtil;
import ca.uhn.fhir.rest.param.QualifierDetails;
import ca.uhn.fhir.rest.param.TokenOrListParam;
@ -199,18 +200,77 @@ public class HfqlExecutor implements IHfqlExecutor {
}
}
/**
* If the user has included a WHERE clause that has a FHIRPath expression but
* could actually be satisfied by a Search Parameter, we'll insert a
* search_match expression so that it's more efficient.
*/
private void massageWhereClauses(HfqlStatement theStatement) {
ResourceSearchParams activeSearchParams =
mySearchParamRegistry.getActiveSearchParams(theStatement.getFromResourceName());
String fromResourceName = theStatement.getFromResourceName();
ResourceSearchParams activeSearchParams = mySearchParamRegistry.getActiveSearchParams(fromResourceName);
for (HfqlStatement.WhereClause nextWhereClause : theStatement.getWhereClauses()) {
String left = null;
List<String> rightValues = null;
String comparator;
if (isDataValueWhereClause(nextWhereClause)) {
if ("id".equals(nextWhereClause.getLeft())) {
left = nextWhereClause.getLeft();
comparator = "";
rightValues = nextWhereClause.getRightAsStrings();
} else if (nextWhereClause.getOperator() == HfqlStatement.WhereClauseOperatorEnum.UNARY_BOOLEAN
&& nextWhereClause.getRightAsStrings().size() > 1) {
left = nextWhereClause.getLeft();
rightValues = nextWhereClause
.getRightAsStrings()
.subList(1, nextWhereClause.getRightAsStrings().size());
switch (nextWhereClause.getRightAsStrings().get(0)) {
case "=":
comparator = "";
break;
case "<":
comparator = ParamPrefixEnum.LESSTHAN.getValue();
break;
case "<=":
comparator = ParamPrefixEnum.LESSTHAN_OR_EQUALS.getValue();
break;
case ">":
comparator = ParamPrefixEnum.GREATERTHAN.getValue();
break;
case ">=":
comparator = ParamPrefixEnum.GREATERTHAN_OR_EQUALS.getValue();
break;
case "!=":
comparator = ParamPrefixEnum.NOT_EQUAL.getValue();
break;
case "~":
comparator = ParamPrefixEnum.APPROXIMATE.getValue();
break;
default:
left = null;
comparator = null;
rightValues = null;
}
} else {
comparator = null;
}
if (left != null) {
if (isFhirPathExpressionEquivalent("id", left, fromResourceName)) {
// This is an expression for Resource.id
nextWhereClause.setLeft("id");
nextWhereClause.setOperator(HfqlStatement.WhereClauseOperatorEnum.SEARCH_MATCH);
String joinedParamValues = nextWhereClause.getRightAsStrings().stream()
.map(ParameterUtil::escape)
String joinedParamValues =
rightValues.stream().map(ParameterUtil::escape).collect(Collectors.joining(","));
nextWhereClause.setRight(Constants.PARAM_ID, joinedParamValues);
} else if (isFhirPathExpressionEquivalent("meta.lastUpdated", left, fromResourceName)) {
// This is an expression for Resource.meta.lastUpdated
nextWhereClause.setLeft("id");
nextWhereClause.setOperator(HfqlStatement.WhereClauseOperatorEnum.SEARCH_MATCH);
String joinedParamValues = rightValues.stream()
.map(value -> comparator + ParameterUtil.escape(value))
.collect(Collectors.joining(","));
nextWhereClause.setRight("_id", joinedParamValues);
nextWhereClause.setRight(Constants.PARAM_LASTUPDATED, joinedParamValues);
}
}
}
@ -490,8 +550,12 @@ public class HfqlExecutor implements IHfqlExecutor {
}
}
} catch (FhirPathExecutionException e) {
String expression =
nextWhereClause.getOperator() == HfqlStatement.WhereClauseOperatorEnum.UNARY_BOOLEAN
? nextWhereClause.asUnaryExpression()
: nextWhereClause.getLeft();
throw new InvalidRequestException(Msg.code(2403) + "Unable to evaluate FHIRPath expression \""
+ nextWhereClause.getLeft() + "\". Error: " + e.getMessage());
+ expression + "\". Error: " + e.getMessage());
}
if (!haveMatch) {
@ -777,6 +841,17 @@ public class HfqlExecutor implements IHfqlExecutor {
return new StaticHfqlExecutionResult(null, columns, dataTypes, rows);
}
private static boolean isFhirPathExpressionEquivalent(
String wantedExpression, String actualExpression, String fromResourceName) {
if (wantedExpression.equals(actualExpression)) {
return true;
}
if (("Resource." + wantedExpression).equals(actualExpression)) {
return true;
}
return (fromResourceName + "." + wantedExpression).equals(actualExpression);
}
/**
* Returns {@literal true} if a where clause has an operator of
* {@link ca.uhn.fhir.jpa.fql.parser.HfqlStatement.WhereClauseOperatorEnum#EQUALS}
@ -796,9 +871,10 @@ public class HfqlExecutor implements IHfqlExecutor {
private static boolean evaluateWhereClauseUnaryBoolean(
HfqlExecutionContext theExecutionContext, IBaseResource r, HfqlStatement.WhereClause theNextWhereClause) {
boolean haveMatch = false;
assert theNextWhereClause.getRight().isEmpty();
List<IPrimitiveType> values =
theExecutionContext.evaluate(r, theNextWhereClause.getLeft(), IPrimitiveType.class);
String fullExpression = theNextWhereClause.asUnaryExpression();
List<IPrimitiveType> values = theExecutionContext.evaluate(r, fullExpression, IPrimitiveType.class);
for (IPrimitiveType<?> nextValue : values) {
if (Boolean.TRUE.equals(nextValue.getValue())) {
haveMatch = true;

View File

@ -139,6 +139,22 @@ class HfqlLexer {
return;
}
for (String nextMultiCharToken : theOptions.getMultiCharTokens()) {
boolean haveStringStartingHere = true;
for (int i = 0; i < nextMultiCharToken.length(); i++) {
if (myInput.length <= myPosition + 1
|| nextMultiCharToken.charAt(i) != myInput[myPosition + i]) {
haveStringStartingHere = false;
break;
}
}
if (haveStringStartingHere) {
setNextToken(theOptions, nextMultiCharToken);
myPosition += nextMultiCharToken.length();
return;
}
}
if (theNextChar == '\'') {
myNextTokenLine = myLine;
myNextTokenColumn = myColumn;

View File

@ -19,6 +19,7 @@
*/
package ca.uhn.fhir.jpa.fql.parser;
import java.util.List;
import java.util.Set;
public enum HfqlLexerOptions {
@ -28,18 +29,20 @@ public enum HfqlLexerOptions {
* more specialized.
*/
HFQL_TOKEN(
List.of(">=", "<=", "!="),
Set.of(
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', '.', '[', ']', '_'),
Set.of(',', '=', '(', ')', '|', ':', '*'),
'8', '9', '.', '[', ']', '_', '~'),
Set.of(',', '=', '(', ')', '|', ':', '*', '<', '>', '!'),
false),
/**
* A FHIR search parameter name.
*/
SEARCH_PARAMETER_NAME(
List.of(),
Set.of(
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
@ -52,12 +55,13 @@ public enum HfqlLexerOptions {
* A complete FHIRPath expression.
*/
FHIRPATH_EXPRESSION(
List.of(">=", "<=", "!="),
Set.of(
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', '.', '[', ']', '_', '(', ')', '!', '~', '<', '>', '+', '-'),
Set.of(',', '|', ':', '*', '='),
'8', '9', '.', '[', ']', '_', '(', ')', '+', '-'),
Set.of(',', '|', ':', '*', '=', '<', '>', '!', '~'),
true),
/**
@ -65,22 +69,26 @@ public enum HfqlLexerOptions {
* dots as separate tokens.
*/
FHIRPATH_EXPRESSION_PART(
List.of(">=", "<=", "!="),
Set.of(
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', '[', ']', '_', '(', ')', '+', '-'),
Set.of(',', '=', '|', ':', '*', '.'),
Set.of(',', '=', '|', ':', '*', '<', '>', '!', '~', '.'),
true);
private final Set<Character> myMultiCharTokenCharacters;
private final boolean mySlurpParens;
private final Set<Character> mySingleCharTokenCharacters;
private final List<String> myMultiCharTokens;
HfqlLexerOptions(
List<String> theMultiCharTokens,
Set<Character> theMultiCharTokenCharacters,
Set<Character> theSingleCharTokenCharacters,
boolean theSlurpParens) {
myMultiCharTokens = theMultiCharTokens;
myMultiCharTokenCharacters = theMultiCharTokenCharacters;
mySingleCharTokenCharacters = theSingleCharTokenCharacters;
mySlurpParens = theSlurpParens;
@ -91,6 +99,14 @@ public enum HfqlLexerOptions {
}
}
/**
* These tokens are always treated as a single token if this string of characters
* is found in sequence
*/
public List<String> getMultiCharTokens() {
return myMultiCharTokens;
}
/**
* These characters are treated as a single character token if they are found
*/

View File

@ -32,6 +32,8 @@ import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.join;
/**
* This class represents a parsed HFQL expression tree. It is useful for
* passing over the wire, but it should not be considered a stable model (in
@ -327,5 +329,14 @@ public class HfqlStatement implements IModelJson {
}
return retVal;
}
/**
* Returns a concatenation of the {@link #getLeft() left} and all of the {@link #getRight() right} expressions,
* each joined by a single string. This is useful for obtaining expressions of
* type {@link WhereClauseOperatorEnum#UNARY_BOOLEAN}.
*/
public String asUnaryExpression() {
return getLeft() + " " + join(getRight(), ' ');
}
}
}

View File

@ -331,10 +331,9 @@ public class HfqlStatementParser {
HfqlLexerToken nextToken = theToken;
if (!KEYWORD_AND.equals(nextToken.asKeyword()) && !DIRECTIVE_KEYWORDS.contains(nextToken.asKeyword())) {
StringBuilder expression = new StringBuilder(myWhereClause.getLeft());
while (true) {
expression.append(' ').append(nextToken.getToken());
myWhereClause.addRight(nextToken.getToken());
while (true) {
if (myLexer.hasNextToken(HfqlLexerOptions.FHIRPATH_EXPRESSION)) {
nextToken = myLexer.getNextToken(HfqlLexerOptions.FHIRPATH_EXPRESSION);
String nextTokenAsKeyword = nextToken.asKeyword();
@ -342,13 +341,12 @@ public class HfqlStatementParser {
|| DIRECTIVE_KEYWORDS.contains(nextTokenAsKeyword)) {
break;
}
myWhereClause.addRight(nextToken.getToken());
} else {
nextToken = null;
break;
}
}
myWhereClause.setLeft(expression.toString());
}
if (nextToken != null) {

View File

@ -0,0 +1,175 @@
package ca.uhn.fhir.jpa.fql.executor;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.rest.server.IPagingProvider;
import ca.uhn.fhir.rest.server.SimpleBundleProvider;
import ca.uhn.fhir.rest.server.util.FhirContextSearchParamRegistry;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.DateType;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Quantity;
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.List;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public abstract class BaseHfqlExecutorTest {
protected final RequestDetails mySrd = new SystemRequestDetails();
@Spy
protected FhirContext myCtx = FhirContext.forR4Cached();
@Mock
protected DaoRegistry myDaoRegistry;
@Mock
protected IPagingProvider myPagingProvider;
@Spy
protected ISearchParamRegistry mySearchParamRegistry = new FhirContextSearchParamRegistry(myCtx);
@InjectMocks
protected HfqlExecutor myHfqlExecutor = new HfqlExecutor();
@Captor
protected ArgumentCaptor<SearchParameterMap> mySearchParameterMapCaptor;
@SuppressWarnings("unchecked")
protected <T extends IBaseResource> IFhirResourceDao<T> initDao(Class<T> theType) {
IFhirResourceDao<T> retVal = mock(IFhirResourceDao.class);
String type = myCtx.getResourceType(theType);
when(myDaoRegistry.getResourceDao(type)).thenReturn(retVal);
return retVal;
}
@Nonnull
protected static List<List<Object>> readAllRowValues(IHfqlExecutionResult result) {
List<List<Object>> rowValues = new ArrayList<>();
while (result.hasNext()) {
rowValues.add(new ArrayList<>(result.getNextRow().getRowValues()));
}
return rowValues;
}
@Nonnull
protected static Observation createCardiologyNoteObservation(String id, String noteText) {
Observation obs = new Observation();
obs.setId(id);
obs.getCode().addCoding()
.setSystem("http://loinc.org")
.setCode("34752-6");
obs.setValue(new StringType(noteText));
return obs;
}
@Nonnull
protected static Observation createWeightObservationWithKilos(String obsId, long kg) {
Observation obs = new Observation();
obs.setId(obsId);
obs.getCode().addCoding()
.setSystem("http://loinc.org")
.setCode("29463-7");
obs.setValue(new Quantity(null, kg, "http://unitsofmeasure.org", "kg", "kg"));
return obs;
}
@Nonnull
protected static SimpleBundleProvider createProviderWithSparseNames() {
Patient patientNoValues = new Patient();
patientNoValues.setActive(true);
Patient patientFamilyNameOnly = new Patient();
patientFamilyNameOnly.addName().setFamily("Simpson");
Patient patientGivenNameOnly = new Patient();
patientGivenNameOnly.addName().addGiven("Homer");
Patient patientBothNames = new Patient();
patientBothNames.addName().setFamily("Simpson").addGiven("Homer");
return new SimpleBundleProvider(List.of(
patientNoValues, patientFamilyNameOnly, patientGivenNameOnly, patientBothNames));
}
@Nonnull
protected static SimpleBundleProvider createProviderWithSomeSimpsonsAndFlanders() {
return new SimpleBundleProvider(
createPatientHomerSimpson(),
createPatientNedFlanders(),
createPatientBartSimpson(),
createPatientLisaSimpson(),
createPatientMaggieSimpson()
);
}
@Nonnull
protected static SimpleBundleProvider createProviderWithSomeSimpsonsAndFlandersWithSomeDuplicates() {
return new SimpleBundleProvider(
createPatientHomerSimpson(),
createPatientHomerSimpson(),
createPatientNedFlanders(),
createPatientNedFlanders(),
createPatientBartSimpson(),
createPatientLisaSimpson(),
createPatientMaggieSimpson());
}
@Nonnull
protected static Patient createPatientMaggieSimpson() {
Patient maggie = new Patient();
maggie.addName().setFamily("Simpson").addGiven("Maggie").addGiven("Evelyn");
maggie.addIdentifier().setSystem("http://system").setValue("value4");
return maggie;
}
@Nonnull
protected static Patient createPatientLisaSimpson() {
Patient lisa = new Patient();
lisa.getMeta().setVersionId("1");
lisa.addName().setFamily("Simpson").addGiven("Lisa").addGiven("Marie");
lisa.addIdentifier().setSystem("http://system").setValue("value3");
return lisa;
}
@Nonnull
protected static Patient createPatientBartSimpson() {
Patient bart = new Patient();
bart.getMeta().setVersionId("3");
bart.addName().setFamily("Simpson").addGiven("Bart").addGiven("El Barto");
bart.addIdentifier().setSystem("http://system").setValue("value2");
return bart;
}
@Nonnull
protected static Patient createPatientNedFlanders() {
Patient nedFlanders = new Patient();
nedFlanders.getMeta().setVersionId("1");
nedFlanders.addName().setFamily("Flanders").addGiven("Ned");
nedFlanders.addIdentifier().setSystem("http://system").setValue("value1");
return nedFlanders;
}
@Nonnull
protected static Patient createPatientHomerSimpson() {
Patient homer = new Patient();
homer.setId("HOMER0");
homer.getMeta().setVersionId("2");
homer.addName().setFamily("Simpson").addGiven("Homer").addGiven("Jay");
homer.addIdentifier().setSystem("http://system").setValue("value0");
homer.setBirthDateElement(new DateType("1950-01-01"));
return homer;
}
}

View File

@ -0,0 +1,103 @@
package ca.uhn.fhir.jpa.fql.executor;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.ParamPrefixEnum;
import ca.uhn.fhir.rest.param.TokenParam;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* We should auto-translate FHIRPath expressions like
* <code>id</code> or <code>meta.lastUpdated</code>
* to an equivalent search parameter since that's more efficient
*/
@ExtendWith(MockitoExtension.class)
public class HfqlExecutorFhirPathTranslationToSearchParamTest extends BaseHfqlExecutorTest {
@ParameterizedTest
@CsvSource(textBlock = """
id , true
Resource.id , true
Resource.id , true
foo.id , false
"""
)
public void testId(String theExpression, boolean theShouldConvert) {
IFhirResourceDao<Patient> patientDao = initDao(Patient.class);
when(patientDao.search(any(), any())).thenReturn(createProviderWithSomeSimpsonsAndFlanders());
String statement = """
SELECT
id, birthDate, meta.lastUpdated
FROM
Patient
WHERE
id = 'ABC123'
""";
statement = statement.replace(" id =", " " + theExpression + " =");
myHfqlExecutor.executeInitialSearch(statement, null, mySrd);
verify(patientDao, times(1)).search(mySearchParameterMapCaptor.capture(), any());
SearchParameterMap map = mySearchParameterMapCaptor.getValue();
if (theShouldConvert) {
assertEquals(1, map.get("_id").size());
assertEquals(1, map.get("_id").get(0).size());
assertNull(((TokenParam) map.get("_id").get(0).get(0)).getSystem());
assertEquals("ABC123", ((TokenParam) map.get("_id").get(0).get(0)).getValue());
} else {
assertNull(map.get("_id"));
}
}
@ParameterizedTest
@CsvSource(textBlock = """
meta.lastUpdated = '2023' , 2023 ,
meta.lastUpdated > '2023' , 2023 , GREATERTHAN
meta.lastUpdated >= '2023' , 2023 , GREATERTHAN_OR_EQUALS
meta.lastUpdated < '2023' , 2023 , LESSTHAN
meta.lastUpdated <= '2023' , 2023 , LESSTHAN_OR_EQUALS
meta.lastUpdated != '2023' , 2023 , NOT_EQUAL
meta.lastUpdated ~ '2023' , 2023 , APPROXIMATE
"""
)
public void testLastUpdated(String theExpression, String theExpectedParamValue, ParamPrefixEnum theExpectedParamPrefix) {
IFhirResourceDao<Patient> patientDao = initDao(Patient.class);
when(patientDao.search(any(), any())).thenReturn(createProviderWithSomeSimpsonsAndFlanders());
String statement = """
SELECT
id, birthDate, meta.lastUpdated
FROM
Patient
WHERE
meta.lastUpdated = '2023'
""";
statement = statement.replace("meta.lastUpdated = '2023'", theExpression);
myHfqlExecutor.executeInitialSearch(statement, null, mySrd);
verify(patientDao, times(1)).search(mySearchParameterMapCaptor.capture(), any());
SearchParameterMap map = mySearchParameterMapCaptor.getValue();
assertEquals(1, map.get("_lastUpdated").size());
assertEquals(1, map.get("_lastUpdated").get(0).size());
assertEquals(theExpectedParamValue, ((DateParam) map.get("_lastUpdated").get(0).get(0)).getValueAsString());
assertEquals(theExpectedParamPrefix, ((DateParam) map.get("_lastUpdated").get(0).get(0)).getPrefix());
}
}

View File

@ -59,22 +59,7 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class HfqlExecutorTest {
private final RequestDetails mySrd = new SystemRequestDetails();
@Spy
private FhirContext myCtx = FhirContext.forR4Cached();
@Mock
private DaoRegistry myDaoRegistry;
@Mock
private IPagingProvider myPagingProvider;
@Spy
private ISearchParamRegistry mySearchParamRegistry = new FhirContextSearchParamRegistry(myCtx);
@InjectMocks
private HfqlExecutor myHfqlExecutor = new HfqlExecutor();
@Captor
private ArgumentCaptor<SearchParameterMap> mySearchParameterMapCaptor;
public class HfqlExecutorTest extends BaseHfqlExecutorTest {
@Test
public void testContinuation() {
@ -1253,126 +1238,4 @@ public class HfqlExecutorTest {
assertErrorMessage(result, "HAPI-2429: Resource type Patient does not have a root element named 'Blah'");
}
@SuppressWarnings("unchecked")
private <T extends IBaseResource> IFhirResourceDao<T> initDao(Class<T> theType) {
IFhirResourceDao<T> retVal = mock(IFhirResourceDao.class);
String type = myCtx.getResourceType(theType);
when(myDaoRegistry.getResourceDao(type)).thenReturn(retVal);
return retVal;
}
@Nonnull
private static List<List<Object>> readAllRowValues(IHfqlExecutionResult result) {
List<List<Object>> rowValues = new ArrayList<>();
while (result.hasNext()) {
rowValues.add(new ArrayList<>(result.getNextRow().getRowValues()));
}
return rowValues;
}
@Nonnull
private static Observation createCardiologyNoteObservation(String id, String noteText) {
Observation obs = new Observation();
obs.setId(id);
obs.getCode().addCoding()
.setSystem("http://loinc.org")
.setCode("34752-6");
obs.setValue(new StringType(noteText));
return obs;
}
@Nonnull
private static Observation createWeightObservationWithKilos(String obsId, long kg) {
Observation obs = new Observation();
obs.setId(obsId);
obs.getCode().addCoding()
.setSystem("http://loinc.org")
.setCode("29463-7");
obs.setValue(new Quantity(null, kg, "http://unitsofmeasure.org", "kg", "kg"));
return obs;
}
@Nonnull
private static SimpleBundleProvider createProviderWithSparseNames() {
Patient patientNoValues = new Patient();
patientNoValues.setActive(true);
Patient patientFamilyNameOnly = new Patient();
patientFamilyNameOnly.addName().setFamily("Simpson");
Patient patientGivenNameOnly = new Patient();
patientGivenNameOnly.addName().addGiven("Homer");
Patient patientBothNames = new Patient();
patientBothNames.addName().setFamily("Simpson").addGiven("Homer");
return new SimpleBundleProvider(List.of(
patientNoValues, patientFamilyNameOnly, patientGivenNameOnly, patientBothNames));
}
@Nonnull
private static SimpleBundleProvider createProviderWithSomeSimpsonsAndFlanders() {
return new SimpleBundleProvider(
createPatientHomerSimpson(),
createPatientNedFlanders(),
createPatientBartSimpson(),
createPatientLisaSimpson(),
createPatientMaggieSimpson()
);
}
@Nonnull
private static SimpleBundleProvider createProviderWithSomeSimpsonsAndFlandersWithSomeDuplicates() {
return new SimpleBundleProvider(
createPatientHomerSimpson(),
createPatientHomerSimpson(),
createPatientNedFlanders(),
createPatientNedFlanders(),
createPatientBartSimpson(),
createPatientLisaSimpson(),
createPatientMaggieSimpson());
}
@Nonnull
private static Patient createPatientMaggieSimpson() {
Patient maggie = new Patient();
maggie.addName().setFamily("Simpson").addGiven("Maggie").addGiven("Evelyn");
maggie.addIdentifier().setSystem("http://system").setValue("value4");
return maggie;
}
@Nonnull
private static Patient createPatientLisaSimpson() {
Patient lisa = new Patient();
lisa.getMeta().setVersionId("1");
lisa.addName().setFamily("Simpson").addGiven("Lisa").addGiven("Marie");
lisa.addIdentifier().setSystem("http://system").setValue("value3");
return lisa;
}
@Nonnull
private static Patient createPatientBartSimpson() {
Patient bart = new Patient();
bart.getMeta().setVersionId("3");
bart.addName().setFamily("Simpson").addGiven("Bart").addGiven("El Barto");
bart.addIdentifier().setSystem("http://system").setValue("value2");
return bart;
}
@Nonnull
private static Patient createPatientNedFlanders() {
Patient nedFlanders = new Patient();
nedFlanders.getMeta().setVersionId("1");
nedFlanders.addName().setFamily("Flanders").addGiven("Ned");
nedFlanders.addIdentifier().setSystem("http://system").setValue("value1");
return nedFlanders;
}
@Nonnull
private static Patient createPatientHomerSimpson() {
Patient homer = new Patient();
homer.setId("HOMER0");
homer.getMeta().setVersionId("2");
homer.addName().setFamily("Simpson").addGiven("Homer").addGiven("Jay");
homer.addIdentifier().setSystem("http://system").setValue("value0");
homer.setBirthDateElement(new DateType("1950-01-01"));
return homer;
}
}

View File

@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import java.util.ArrayList;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
@ -144,6 +145,76 @@ public class HfqlLexerTest {
assertEquals("( Observation.value.ofType ( Quantity ) ).unit", lexer.getNextToken(HfqlLexerOptions.FHIRPATH_EXPRESSION).getToken());
}
@ParameterizedTest
@CsvSource(textBlock = """
>= , false , HFQL_TOKEN
<= , false , HFQL_TOKEN
!= , false , HFQL_TOKEN
= , false , HFQL_TOKEN
>= , true , HFQL_TOKEN
<= , true , HFQL_TOKEN
!= , true , HFQL_TOKEN
~ , true , HFQL_TOKEN
= , true , HFQL_TOKEN
>= , false , FHIRPATH_EXPRESSION
<= , false , FHIRPATH_EXPRESSION
!= , false , FHIRPATH_EXPRESSION
= , false , FHIRPATH_EXPRESSION
>= , true , FHIRPATH_EXPRESSION
<= , true , FHIRPATH_EXPRESSION
!= , true , FHIRPATH_EXPRESSION
~ , true , FHIRPATH_EXPRESSION
= , true , FHIRPATH_EXPRESSION
>= , false , FHIRPATH_EXPRESSION_PART
<= , false , FHIRPATH_EXPRESSION_PART
!= , false , FHIRPATH_EXPRESSION_PART
= , false , FHIRPATH_EXPRESSION_PART
>= , true , FHIRPATH_EXPRESSION_PART
<= , true , FHIRPATH_EXPRESSION_PART
!= , true , FHIRPATH_EXPRESSION_PART
~ , true , FHIRPATH_EXPRESSION_PART
= , true , FHIRPATH_EXPRESSION_PART
"""
)
void testComparators(String theComparator, boolean thePad, HfqlLexerOptions theOptions) {
String input = """
SELECT
id
FROM
Patient
WHERE
meta.lastUpdated >= '2023-10-09'
""";
String comparator = theComparator.trim();
if (thePad) {
input = input.replace(" >= ", " " + comparator + " ");
} else {
input = input.replace(" >= ", comparator);
}
List<String> allTokens = new HfqlLexer(input).allTokens(theOptions);
List<String> expectedItems = new ArrayList<>();
expectedItems.add("SELECT");
expectedItems.add("id");
expectedItems.add("FROM");
expectedItems.add("Patient");
expectedItems.add("WHERE");
if (theOptions == HfqlLexerOptions.FHIRPATH_EXPRESSION_PART) {
expectedItems.add("meta");
expectedItems.add(".");
expectedItems.add("lastUpdated");
} else {
expectedItems.add("meta.lastUpdated");
}
expectedItems.add(comparator);
expectedItems.add("'2023-10-09'");
assertThat(allTokens.toString(), allTokens, contains(expectedItems.toArray(new String[0])));
}
@ParameterizedTest
@CsvSource({
"token1 token2 'token3, HFQL_TOKEN",

View File

@ -246,9 +246,9 @@ public class HfqlStatementParserTest {
HfqlStatement statement = parse(input);
assertEquals(1, statement.getWhereClauses().size());
assertEquals("value.ofType(Quantity).value > 100", statement.getWhereClauses().get(0).getLeft());
assertEquals("value.ofType(Quantity).value", statement.getWhereClauses().get(0).getLeft());
assertThat(statement.getWhereClauses().get(0).getRightAsStrings(), contains(">", "100"));
assertEquals(HfqlStatement.WhereClauseOperatorEnum.UNARY_BOOLEAN, statement.getWhereClauses().get(0).getOperator());
assertEquals(0, statement.getWhereClauses().get(0).getRight().size());
}
@Test