mirror of
https://github.com/hapifhir/hapi-fhir.git
synced 2025-02-16 09:55:09 +00:00
LastUpdated search doesn't work with HFQL (#5510)
* LastUpdated search doesn't work with HFQL * Spotless
This commit is contained in:
parent
c4ac940e14
commit
d187399ce5
@ -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."
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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(), ' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user