Improve ID filtering (#5215)
* Improve ID filtering * Formatting * Add license header * Address review comments
This commit is contained in:
parent
3512f82a6d
commit
1e45506526
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
type: perf
|
||||||
|
issue: 5215
|
||||||
|
title: "When executing an HFQL search with a FHIRPath filter on the `id` element, this will now
|
||||||
|
be automatically converted into an `_id` search parameter match for better performance."
|
|
@ -1,3 +1,22 @@
|
||||||
|
/*-
|
||||||
|
* #%L
|
||||||
|
* HAPI FHIR JPA Server
|
||||||
|
* %%
|
||||||
|
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||||
|
* %%
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
* #L%
|
||||||
|
*/
|
||||||
package ca.uhn.fhir.jpa.search.builder.models;
|
package ca.uhn.fhir.jpa.search.builder.models;
|
||||||
|
|
||||||
import ca.uhn.fhir.jpa.search.builder.ISearchQueryExecutor;
|
import ca.uhn.fhir.jpa.search.builder.ISearchQueryExecutor;
|
||||||
|
|
|
@ -42,11 +42,13 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||||
import ca.uhn.fhir.rest.param.DateOrListParam;
|
import ca.uhn.fhir.rest.param.DateOrListParam;
|
||||||
import ca.uhn.fhir.rest.param.DateParam;
|
import ca.uhn.fhir.rest.param.DateParam;
|
||||||
|
import ca.uhn.fhir.rest.param.ParameterUtil;
|
||||||
import ca.uhn.fhir.rest.param.QualifierDetails;
|
import ca.uhn.fhir.rest.param.QualifierDetails;
|
||||||
import ca.uhn.fhir.rest.param.TokenOrListParam;
|
import ca.uhn.fhir.rest.param.TokenOrListParam;
|
||||||
import ca.uhn.fhir.rest.server.IPagingProvider;
|
import ca.uhn.fhir.rest.server.IPagingProvider;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||||
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
|
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
|
||||||
|
import ca.uhn.fhir.rest.server.util.ResourceSearchParams;
|
||||||
import ca.uhn.fhir.util.UrlUtil;
|
import ca.uhn.fhir.util.UrlUtil;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import org.apache.commons.collections4.ListUtils;
|
import org.apache.commons.collections4.ListUtils;
|
||||||
|
@ -142,6 +144,8 @@ public class HfqlExecutor implements IHfqlExecutor {
|
||||||
|
|
||||||
massageSelectColumnNames(statement);
|
massageSelectColumnNames(statement);
|
||||||
populateSelectColumnDataTypes(statement);
|
populateSelectColumnDataTypes(statement);
|
||||||
|
validateWhereClauses(statement);
|
||||||
|
massageWhereClauses(statement);
|
||||||
|
|
||||||
SearchParameterMap map = new SearchParameterMap();
|
SearchParameterMap map = new SearchParameterMap();
|
||||||
addHfqlWhereClausesToSearchParameterMap(statement, map);
|
addHfqlWhereClausesToSearchParameterMap(statement, map);
|
||||||
|
@ -179,6 +183,39 @@ public class HfqlExecutor implements IHfqlExecutor {
|
||||||
return executionResult;
|
return executionResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void validateWhereClauses(HfqlStatement theStatement) {
|
||||||
|
for (HfqlStatement.WhereClause next : theStatement.getWhereClauses()) {
|
||||||
|
if (isDataValueWhereClause(next)) {
|
||||||
|
if (next.getLeft().matches("^[a-zA-Z]+$")) {
|
||||||
|
RuntimeResourceDefinition resDef =
|
||||||
|
myFhirContext.getResourceDefinition(theStatement.getFromResourceName());
|
||||||
|
if (resDef.getChildByName(next.getLeft()) == null) {
|
||||||
|
throw new InvalidRequestException(
|
||||||
|
Msg.code(2429) + "Resource type " + theStatement.getFromResourceName()
|
||||||
|
+ " does not have a root element named '" + next.getLeft() + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void massageWhereClauses(HfqlStatement theStatement) {
|
||||||
|
ResourceSearchParams activeSearchParams =
|
||||||
|
mySearchParamRegistry.getActiveSearchParams(theStatement.getFromResourceName());
|
||||||
|
|
||||||
|
for (HfqlStatement.WhereClause nextWhereClause : theStatement.getWhereClauses()) {
|
||||||
|
if (isDataValueWhereClause(nextWhereClause)) {
|
||||||
|
if ("id".equals(nextWhereClause.getLeft())) {
|
||||||
|
nextWhereClause.setOperator(HfqlStatement.WhereClauseOperatorEnum.SEARCH_MATCH);
|
||||||
|
String joinedParamValues = nextWhereClause.getRightAsStrings().stream()
|
||||||
|
.map(ParameterUtil::escape)
|
||||||
|
.collect(Collectors.joining(","));
|
||||||
|
nextWhereClause.setRight("_id", joinedParamValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void addHfqlWhereClausesToSearchParameterMap(HfqlStatement statement, SearchParameterMap map) {
|
private void addHfqlWhereClausesToSearchParameterMap(HfqlStatement statement, SearchParameterMap map) {
|
||||||
List<HfqlStatement.WhereClause> searchClauses = statement.getWhereClauses();
|
List<HfqlStatement.WhereClause> searchClauses = statement.getWhereClauses();
|
||||||
for (HfqlStatement.WhereClause nextSearchClause : searchClauses) {
|
for (HfqlStatement.WhereClause nextSearchClause : searchClauses) {
|
||||||
|
@ -740,6 +777,17 @@ public class HfqlExecutor implements IHfqlExecutor {
|
||||||
return new StaticHfqlExecutionResult(null, columns, dataTypes, rows);
|
return new StaticHfqlExecutionResult(null, columns, dataTypes, rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@literal true} if a where clause has an operator of
|
||||||
|
* {@link ca.uhn.fhir.jpa.fql.parser.HfqlStatement.WhereClauseOperatorEnum#EQUALS}
|
||||||
|
* or
|
||||||
|
* {@link ca.uhn.fhir.jpa.fql.parser.HfqlStatement.WhereClauseOperatorEnum#IN}
|
||||||
|
*/
|
||||||
|
private static boolean isDataValueWhereClause(HfqlStatement.WhereClause next) {
|
||||||
|
return next.getOperator() == HfqlStatement.WhereClauseOperatorEnum.EQUALS
|
||||||
|
|| next.getOperator() == HfqlStatement.WhereClauseOperatorEnum.IN;
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
static Comparator<IHfqlExecutionResult.Row> newRowComparator(int columnIndex, HfqlDataTypeEnum dataType) {
|
static Comparator<IHfqlExecutionResult.Row> newRowComparator(int columnIndex, HfqlDataTypeEnum dataType) {
|
||||||
return Comparator.comparing(new RowValueExtractor(columnIndex, dataType));
|
return Comparator.comparing(new RowValueExtractor(columnIndex, dataType));
|
||||||
|
|
|
@ -26,6 +26,7 @@ import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
|
@ -303,6 +304,11 @@ public class HfqlStatement implements IModelJson {
|
||||||
return myRight;
|
return myRight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setRight(String... theValues) {
|
||||||
|
myRight.clear();
|
||||||
|
myRight.addAll(Arrays.asList(theValues));
|
||||||
|
}
|
||||||
|
|
||||||
public void addRight(String theRight) {
|
public void addRight(String theRight) {
|
||||||
myRight.add(theRight);
|
myRight.add(theRight);
|
||||||
}
|
}
|
||||||
|
|
|
@ -924,7 +924,7 @@ public class HfqlExecutorTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testWhere_Id_In_CommaList() {
|
public void testWhere_Id_In_CommaList_SearchMatch() {
|
||||||
IFhirResourceDao<Observation> patientDao = initDao(Observation.class);
|
IFhirResourceDao<Observation> patientDao = initDao(Observation.class);
|
||||||
Observation resource = new Observation();
|
Observation resource = new Observation();
|
||||||
resource.getMeta().setVersionId("5");
|
resource.getMeta().setVersionId("5");
|
||||||
|
@ -961,6 +961,34 @@ public class HfqlExecutorTest {
|
||||||
assertEquals("Patient/456", ((TokenParam) map.get("_id").get(0).get(1)).getValue());
|
assertEquals("Patient/456", ((TokenParam) map.get("_id").get(0).get(1)).getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testWhere_FhirPathElevatedToSearchParam_Id_Equals() {
|
||||||
|
IFhirResourceDao<Patient> patientDao = initDao(Patient.class);
|
||||||
|
when(patientDao.search(any(), any())).thenReturn(createProviderWithSomeSimpsonsAndFlanders());
|
||||||
|
|
||||||
|
String statement = """
|
||||||
|
select id
|
||||||
|
from Patient
|
||||||
|
where id IN ('HOMER0', 'HOMER1')
|
||||||
|
""";
|
||||||
|
|
||||||
|
IHfqlExecutionResult result = myHfqlExecutor.executeInitialSearch(statement, null, mySrd);
|
||||||
|
|
||||||
|
assertTrue(result.hasNext());
|
||||||
|
List<Object> nextRow = result.getNextRow().getRowValues();
|
||||||
|
assertEquals("HOMER0", nextRow.get(0));
|
||||||
|
|
||||||
|
verify(patientDao, times(1)).search(mySearchParameterMapCaptor.capture(), any());
|
||||||
|
SearchParameterMap map = mySearchParameterMapCaptor.getValue();
|
||||||
|
assertEquals(1, map.get("_id").size());
|
||||||
|
assertEquals(2, map.get("_id").get(0).size());
|
||||||
|
assertNull(((TokenParam) map.get("_id").get(0).get(0)).getSystem());
|
||||||
|
assertEquals("HOMER0", ((TokenParam) map.get("_id").get(0).get(0)).getValue());
|
||||||
|
assertNull(((TokenParam) map.get("_id").get(0).get(1)).getSystem());
|
||||||
|
assertEquals("HOMER1", ((TokenParam) map.get("_id").get(0).get(1)).getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSearch_QualifiedSelect() {
|
public void testSearch_QualifiedSelect() {
|
||||||
IFhirResourceDao<Patient> patientDao = initDao(Patient.class);
|
IFhirResourceDao<Patient> patientDao = initDao(Patient.class);
|
||||||
|
@ -1211,6 +1239,20 @@ public class HfqlExecutorTest {
|
||||||
assertErrorMessage(result, "HAPI-2413: search_match function requires 2 arguments");
|
assertErrorMessage(result, "HAPI-2413: search_match function requires 2 arguments");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testError_InvalidWhereParameter() {
|
||||||
|
initDao(Patient.class);
|
||||||
|
|
||||||
|
String input = """
|
||||||
|
select name.family
|
||||||
|
from Patient
|
||||||
|
where Blah = '123'
|
||||||
|
""";
|
||||||
|
|
||||||
|
IHfqlExecutionResult result = myHfqlExecutor.executeInitialSearch(input, null, mySrd);
|
||||||
|
assertErrorMessage(result, "HAPI-2429: Resource type Patient does not have a root element named 'Blah'");
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private <T extends IBaseResource> IFhirResourceDao<T> initDao(Class<T> theType) {
|
private <T extends IBaseResource> IFhirResourceDao<T> initDao(Class<T> theType) {
|
||||||
IFhirResourceDao<T> retVal = mock(IFhirResourceDao.class);
|
IFhirResourceDao<T> retVal = mock(IFhirResourceDao.class);
|
||||||
|
@ -1325,6 +1367,7 @@ public class HfqlExecutorTest {
|
||||||
@Nonnull
|
@Nonnull
|
||||||
private static Patient createPatientHomerSimpson() {
|
private static Patient createPatientHomerSimpson() {
|
||||||
Patient homer = new Patient();
|
Patient homer = new Patient();
|
||||||
|
homer.setId("HOMER0");
|
||||||
homer.getMeta().setVersionId("2");
|
homer.getMeta().setVersionId("2");
|
||||||
homer.addName().setFamily("Simpson").addGiven("Homer").addGiven("Jay");
|
homer.addName().setFamily("Simpson").addGiven("Homer").addGiven("Jay");
|
||||||
homer.addIdentifier().setSystem("http://system").setValue("value0");
|
homer.addIdentifier().setSystem("http://system").setValue("value0");
|
||||||
|
|
|
@ -384,7 +384,7 @@ public class Controller extends BaseController {
|
||||||
final ModelMap theModel) {
|
final ModelMap theModel) {
|
||||||
addCommonParamsForHfql(theServletRequest, theRequest, theModel);
|
addCommonParamsForHfql(theServletRequest, theRequest, theModel);
|
||||||
|
|
||||||
ourLog.info("Executing HFQL query: {}", theHfqlQuery);
|
ourLog.info("Executing HFQL query: {}", theHfqlQuery.replaceAll("\\s+", " "));
|
||||||
StopWatch sw = new StopWatch();
|
StopWatch sw = new StopWatch();
|
||||||
|
|
||||||
List<List<String>> rows = new ArrayList<>();
|
List<List<String>> rows = new ArrayList<>();
|
||||||
|
|
|
@ -41,16 +41,7 @@
|
||||||
|
|
||||||
<div th:replace="tmpl-banner :: banner"></div>
|
<div th:replace="tmpl-banner :: banner"></div>
|
||||||
|
|
||||||
<div class="card" style="margin-top: 10px;">
|
<div th:replace="tmpl-hfql-banner-card :: banner"></div>
|
||||||
<h3 class="card-header">HFQL / SQL Execution</h3>
|
|
||||||
<div class="card-body">
|
|
||||||
This page can be used to execute queries using the HAPI FHIR Query Language (HFQL),
|
|
||||||
which is a SQL-like syntax for querying FHIR repositories. Learn more about
|
|
||||||
the HFQL syntax at:
|
|
||||||
<a href="https://smilecdr.com/docs/hfql/">https://smilecdr.com/docs/hfql/</a>.
|
|
||||||
This UI will display a maximum of [[${rowLimit}]] rows.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ************************************************ -->
|
<!-- ************************************************ -->
|
||||||
<!-- ** SQL Editor ** -->
|
<!-- ** SQL Editor ** -->
|
||||||
|
@ -71,8 +62,6 @@
|
||||||
<script th:src="@{/resources/ace-builds/src-min-noconflict/ace.js}"></script>
|
<script th:src="@{/resources/ace-builds/src-min-noconflict/ace.js}"></script>
|
||||||
<script>
|
<script>
|
||||||
const editor = ace.edit("editor");
|
const editor = ace.edit("editor");
|
||||||
editor.setTheme("ace/theme/eclipse");
|
|
||||||
editor.setTheme("ace/theme/vibrant_ink");
|
|
||||||
editor.setTheme("ace/theme/cobalt");
|
editor.setTheme("ace/theme/cobalt");
|
||||||
editor.session.setMode("ace/mode/sql");
|
editor.session.setMode("ace/mode/sql");
|
||||||
editor.setShowPrintMargin(false);
|
editor.setShowPrintMargin(false);
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<div class="card" style="margin-top: 10px;" th:fragment="banner">
|
||||||
|
<h3 class="card-header">HFQL / SQL Execution</h3>
|
||||||
|
<div class="card-body">
|
||||||
|
This page can be used to execute queries using the HAPI FHIR Query Language (HFQL),
|
||||||
|
which is a SQL-like syntax for querying FHIR repositories. Learn more about
|
||||||
|
HFQL architecture at <a href="https://smilecdr.com/docs/hfql/">https://smilecdr.com/docs/hfql/</a>, and
|
||||||
|
learn more about the HFQL syntax at <a href="https://smilecdr.com/docs/hfql/sql_syntax.html">https://smilecdr.com/docs/hfql/sql_syntax.html</a>.
|
||||||
|
This UI will display a maximum of [[${rowLimit}]] rows.
|
||||||
|
</div>
|
||||||
|
</div>
|
Loading…
Reference in New Issue