Combo Parameter Permutations (#6073)
* Combo permutations * Work on combo * Add tests * Add changelog * Spotless * Test fix * Drop label * Add tests * Spotles * Account for review comments
This commit is contained in:
parent
8a41da4a18
commit
5e519810ff
|
@ -19,7 +19,7 @@
|
|||
*/
|
||||
package ca.uhn.fhir.rest.param;
|
||||
|
||||
import ca.uhn.fhir.util.CoverageIgnore;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
|
||||
public class DateAndListParam extends BaseAndListParam<DateOrListParam> {
|
||||
|
||||
|
@ -28,10 +28,24 @@ public class DateAndListParam extends BaseAndListParam<DateOrListParam> {
|
|||
return new DateOrListParam();
|
||||
}
|
||||
|
||||
@CoverageIgnore
|
||||
@Override
|
||||
public DateAndListParam addAnd(DateOrListParam theValue) {
|
||||
addValue(theValue);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param theValue The OR values
|
||||
* @return Returns a reference to this for convenient chaining
|
||||
* @since 7.4.0
|
||||
*/
|
||||
public DateAndListParam addAnd(DateParam... theValue) {
|
||||
Validate.notNull(theValue, "theValue must not be null");
|
||||
DateOrListParam orListParam = new DateOrListParam();
|
||||
for (DateParam next : theValue) {
|
||||
orListParam.add(next);
|
||||
}
|
||||
addValue(orListParam);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
type: add
|
||||
issue: 6073
|
||||
title: "Previously, if a unique or non-unique combo SeaerchParameter was
|
||||
defined, it would not be used by searches if any parameter contained
|
||||
multiple OR clauses (e.g. `Patient?family=simpson&given=homer,marge`).
|
||||
Such searches will now use the combo index table, which should result
|
||||
in much more performant searches in some cases."
|
|
@ -21,6 +21,7 @@ You may use the following command to get detailed help on the options:
|
|||
Note the arguments:
|
||||
|
||||
* `-d [dialect]` – This indicates the database dialect to use. See the detailed help for a list of options
|
||||
* `--enable-heavyweight-migrations` – If this flag is set, additional migration tasks will be executed that are considered unnecessary to execute on a database with a significant amount of data loaded. This option is not generally necessary.
|
||||
|
||||
# Oracle Support
|
||||
|
||||
|
|
|
@ -2779,16 +2779,16 @@ public class QueryStack {
|
|||
}
|
||||
}
|
||||
|
||||
public void addPredicateCompositeUnique(String theIndexString, RequestPartitionId theRequestPartitionId) {
|
||||
public void addPredicateCompositeUnique(List<String> theIndexStrings, RequestPartitionId theRequestPartitionId) {
|
||||
ComboUniqueSearchParameterPredicateBuilder predicateBuilder = mySqlBuilder.addComboUniquePredicateBuilder();
|
||||
Condition predicate = predicateBuilder.createPredicateIndexString(theRequestPartitionId, theIndexString);
|
||||
Condition predicate = predicateBuilder.createPredicateIndexString(theRequestPartitionId, theIndexStrings);
|
||||
mySqlBuilder.addPredicate(predicate);
|
||||
}
|
||||
|
||||
public void addPredicateCompositeNonUnique(String theIndexString, RequestPartitionId theRequestPartitionId) {
|
||||
public void addPredicateCompositeNonUnique(List<String> theIndexStrings, RequestPartitionId theRequestPartitionId) {
|
||||
ComboNonUniqueSearchParameterPredicateBuilder predicateBuilder =
|
||||
mySqlBuilder.addComboNonUniquePredicateBuilder();
|
||||
Condition predicate = predicateBuilder.createPredicateHashComplete(theRequestPartitionId, theIndexString);
|
||||
Condition predicate = predicateBuilder.createPredicateHashComplete(theRequestPartitionId, theIndexStrings);
|
||||
mySqlBuilder.addPredicate(predicate);
|
||||
}
|
||||
|
||||
|
|
|
@ -68,6 +68,7 @@ import ca.uhn.fhir.jpa.searchparam.util.Dstu3DistanceHelper;
|
|||
import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil;
|
||||
import ca.uhn.fhir.jpa.searchparam.util.LastNParameterHelper;
|
||||
import ca.uhn.fhir.jpa.util.BaseIterator;
|
||||
import ca.uhn.fhir.jpa.util.CartesianProductUtil;
|
||||
import ca.uhn.fhir.jpa.util.CurrentThreadCaptureQueriesListener;
|
||||
import ca.uhn.fhir.jpa.util.QueryChunker;
|
||||
import ca.uhn.fhir.jpa.util.SqlQueryList;
|
||||
|
@ -83,6 +84,7 @@ import ca.uhn.fhir.rest.api.SortOrderEnum;
|
|||
import ca.uhn.fhir.rest.api.SortSpec;
|
||||
import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.param.BaseParamWithPrefix;
|
||||
import ca.uhn.fhir.rest.param.DateParam;
|
||||
import ca.uhn.fhir.rest.param.DateRangeParam;
|
||||
import ca.uhn.fhir.rest.param.ParameterUtil;
|
||||
|
@ -98,6 +100,7 @@ import ca.uhn.fhir.util.StopWatch;
|
|||
import ca.uhn.fhir.util.StringUtil;
|
||||
import ca.uhn.fhir.util.UrlUtil;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Streams;
|
||||
import com.healthmarketscience.sqlbuilder.Condition;
|
||||
import jakarta.annotation.Nonnull;
|
||||
|
@ -125,6 +128,7 @@ import org.springframework.transaction.support.TransactionSynchronizationManager
|
|||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
|
@ -137,6 +141,7 @@ import java.util.stream.Collectors;
|
|||
import static ca.uhn.fhir.jpa.model.util.JpaConstants.UNDESIRED_RESOURCE_LINKAGES_FOR_EVERYTHING_ON_PATIENT_INSTANCE;
|
||||
import static ca.uhn.fhir.jpa.search.builder.QueryStack.LOCATION_POSITION;
|
||||
import static ca.uhn.fhir.jpa.search.builder.QueryStack.SearchForIdsParams.with;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.apache.commons.lang3.StringUtils.defaultString;
|
||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
|
@ -310,9 +315,7 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
|||
* parameters all have no modifiers.
|
||||
*/
|
||||
private boolean isCompositeUniqueSpCandidate() {
|
||||
return myStorageSettings.isUniqueIndexesEnabled()
|
||||
&& myParams.getEverythingMode() == null
|
||||
&& myParams.isAllParametersHaveNoModifier();
|
||||
return myStorageSettings.isUniqueIndexesEnabled() && myParams.getEverythingMode() == null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
|
@ -1910,13 +1913,29 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
|||
if (comboParam != null) {
|
||||
Collections.sort(comboParamNames);
|
||||
|
||||
if (!validateParamValuesAreValidForComboParam(theParams, comboParamNames)) {
|
||||
return;
|
||||
}
|
||||
// Since we're going to remove elements below
|
||||
theParams.values().forEach(this::ensureSubListsAreWritable);
|
||||
|
||||
/*
|
||||
* Apply search against the combo param index in a loop:
|
||||
*
|
||||
* 1. First we check whether the actual parameter values in the
|
||||
* parameter map are actually usable for searching against the combo
|
||||
* param index. E.g. no search modifiers, date comparators, etc.,
|
||||
* since these mean you can't use the combo index.
|
||||
*
|
||||
* 2. Apply and create the join SQl. We remove parameter values from
|
||||
* the map as we apply them, so any parameter values remaining in the
|
||||
* map after each loop haven't yet been factored into the SQL.
|
||||
*
|
||||
* The loop allows us to create multiple combo index joins if there
|
||||
* are multiple AND expressions for the related parameters.
|
||||
*/
|
||||
while (validateParamValuesAreValidForComboParam(theRequest, theParams, comboParamNames)) {
|
||||
applyComboSearchParam(theQueryStack, theParams, theRequest, comboParamNames, comboParam);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void applyComboSearchParam(
|
||||
QueryStack theQueryStack,
|
||||
|
@ -1924,34 +1943,30 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
|||
RequestDetails theRequest,
|
||||
List<String> theComboParamNames,
|
||||
RuntimeSearchParam theComboParam) {
|
||||
// Since we're going to remove elements below
|
||||
theParams.values().forEach(this::ensureSubListsAreWritable);
|
||||
|
||||
StringBuilder theSearchBuilder = new StringBuilder();
|
||||
theSearchBuilder.append(myResourceName);
|
||||
theSearchBuilder.append("?");
|
||||
|
||||
boolean first = true;
|
||||
List<List<IQueryParameterType>> inputs = new ArrayList<>();
|
||||
for (String nextParamName : theComboParamNames) {
|
||||
List<List<IQueryParameterType>> nextValues = theParams.get(nextParamName);
|
||||
|
||||
// This should never happen, but this safety check was added along the way and
|
||||
// presumably must save us in some specific race condition. I am preserving it
|
||||
// in a refactor of this code base. 20240429
|
||||
if (nextValues.isEmpty()) {
|
||||
ourLog.error(
|
||||
"query parameter {} is unexpectedly empty. Encountered while considering {} index for {}",
|
||||
nextParamName,
|
||||
theComboParam.getName(),
|
||||
theRequest.getCompleteUrl());
|
||||
continue;
|
||||
List<IQueryParameterType> nextValues = theParams.get(nextParamName).remove(0);
|
||||
inputs.add(nextValues);
|
||||
}
|
||||
|
||||
List<? extends IQueryParameterType> nextAnd = nextValues.remove(0);
|
||||
IQueryParameterType nextOr = nextAnd.remove(0);
|
||||
List<List<IQueryParameterType>> inputPermutations = Lists.cartesianProduct(inputs);
|
||||
List<String> indexStrings = new ArrayList<>(CartesianProductUtil.calculateCartesianProductSize(inputs));
|
||||
for (List<IQueryParameterType> nextPermutation : inputPermutations) {
|
||||
|
||||
StringBuilder searchStringBuilder = new StringBuilder();
|
||||
searchStringBuilder.append(myResourceName);
|
||||
searchStringBuilder.append("?");
|
||||
|
||||
boolean first = true;
|
||||
for (int paramIndex = 0; paramIndex < theComboParamNames.size(); paramIndex++) {
|
||||
|
||||
String nextParamName = theComboParamNames.get(paramIndex);
|
||||
IQueryParameterType nextOr = nextPermutation.get(paramIndex);
|
||||
String nextOrValue = nextOr.getValueAsQueryToken(myContext);
|
||||
|
||||
RuntimeSearchParam nextParamDef = mySearchParamRegistry.getActiveSearchParam(myResourceName, nextParamName);
|
||||
RuntimeSearchParam nextParamDef =
|
||||
mySearchParamRegistry.getActiveSearchParam(myResourceName, nextParamName);
|
||||
if (theComboParam.getComboSearchParamType() == ComboSearchParamType.NON_UNIQUE) {
|
||||
if (nextParamDef.getParamType() == RestSearchParameterTypeEnum.STRING) {
|
||||
nextOrValue = StringUtil.normalizeStringForSearchIndexing(nextOrValue);
|
||||
|
@ -1961,24 +1976,30 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
|||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
theSearchBuilder.append('&');
|
||||
searchStringBuilder.append('&');
|
||||
}
|
||||
|
||||
nextParamName = UrlUtil.escapeUrlParam(nextParamName);
|
||||
nextOrValue = UrlUtil.escapeUrlParam(nextOrValue);
|
||||
|
||||
theSearchBuilder.append(nextParamName).append('=').append(nextOrValue);
|
||||
searchStringBuilder.append(nextParamName).append('=').append(nextOrValue);
|
||||
}
|
||||
|
||||
if (theSearchBuilder != null) {
|
||||
String indexString = theSearchBuilder.toString();
|
||||
String indexString = searchStringBuilder.toString();
|
||||
ourLog.debug(
|
||||
"Checking for {} combo index for query: {}", theComboParam.getComboSearchParamType(), indexString);
|
||||
|
||||
indexStrings.add(indexString);
|
||||
}
|
||||
|
||||
// Just to make sure we're stable for tests
|
||||
indexStrings.sort(Comparator.naturalOrder());
|
||||
|
||||
// Interceptor broadcast: JPA_PERFTRACE_INFO
|
||||
String indexStringForLog = indexStrings.size() > 1 ? indexStrings.toString() : indexStrings.get(0);
|
||||
StorageProcessingMessage msg = new StorageProcessingMessage()
|
||||
.setMessage("Using " + theComboParam.getComboSearchParamType() + " index for query for search: "
|
||||
+ indexString);
|
||||
.setMessage("Using " + theComboParam.getComboSearchParamType() + " index(es) for query for search: "
|
||||
+ indexStringForLog);
|
||||
HookParams params = new HookParams()
|
||||
.add(RequestDetails.class, theRequest)
|
||||
.addIfMatchesType(ServletRequestDetails.class, theRequest)
|
||||
|
@ -1986,44 +2007,73 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
|||
CompositeInterceptorBroadcaster.doCallHooks(
|
||||
myInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_INFO, params);
|
||||
|
||||
switch (theComboParam.getComboSearchParamType()) {
|
||||
switch (requireNonNull(theComboParam.getComboSearchParamType())) {
|
||||
case UNIQUE:
|
||||
theQueryStack.addPredicateCompositeUnique(indexString, myRequestPartitionId);
|
||||
theQueryStack.addPredicateCompositeUnique(indexStrings, myRequestPartitionId);
|
||||
break;
|
||||
case NON_UNIQUE:
|
||||
theQueryStack.addPredicateCompositeNonUnique(indexString, myRequestPartitionId);
|
||||
theQueryStack.addPredicateCompositeNonUnique(indexStrings, myRequestPartitionId);
|
||||
break;
|
||||
}
|
||||
|
||||
// Remove any empty parameters remaining after this
|
||||
theParams.clean();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@literal true} if the actual parameter instances in a given query are actually usable for
|
||||
* searching against a combo param with the given parameter names. This might be {@literal false} if
|
||||
* parameters have modifiers (e.g. <code>?name:exact=SIMPSON</code>), prefixes
|
||||
* (e.g. <code>?date=gt2024-02-01</code>), etc.
|
||||
*/
|
||||
private boolean validateParamValuesAreValidForComboParam(
|
||||
@Nonnull SearchParameterMap theParams, List<String> comboParamNames) {
|
||||
RequestDetails theRequest, @Nonnull SearchParameterMap theParams, List<String> theComboParamNames) {
|
||||
boolean paramValuesAreValidForCombo = true;
|
||||
for (String nextParamName : comboParamNames) {
|
||||
List<List<IQueryParameterType>> paramOrValues = new ArrayList<>(theComboParamNames.size());
|
||||
|
||||
for (String nextParamName : theComboParamNames) {
|
||||
List<List<IQueryParameterType>> nextValues = theParams.get(nextParamName);
|
||||
|
||||
// Multiple AND parameters are not supported for unique combo params
|
||||
if (nextValues.get(0).size() != 1) {
|
||||
ourLog.debug(
|
||||
"Search is not a candidate for unique combo searching - Multiple AND expressions found for the same parameter");
|
||||
if (nextValues == null || nextValues.isEmpty()) {
|
||||
paramValuesAreValidForCombo = false;
|
||||
break;
|
||||
}
|
||||
|
||||
List<IQueryParameterType> nextAndValue = nextValues.get(0);
|
||||
paramOrValues.add(nextAndValue);
|
||||
|
||||
for (IQueryParameterType nextOrValue : nextAndValue) {
|
||||
if (nextOrValue instanceof DateParam) {
|
||||
if (((DateParam) nextOrValue).getPrecision() != TemporalPrecisionEnum.DAY) {
|
||||
ourLog.debug(
|
||||
"Search is not a candidate for unique combo searching - Date search with non-DAY precision");
|
||||
DateParam dateParam = (DateParam) nextOrValue;
|
||||
if (dateParam.getPrecision() != TemporalPrecisionEnum.DAY) {
|
||||
String message = "Search with params " + theComboParamNames
|
||||
+ " is not a candidate for combo searching - Date search with non-DAY precision for parameter '"
|
||||
+ nextParamName + "'";
|
||||
firePerformanceInfo(theRequest, message);
|
||||
paramValuesAreValidForCombo = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (nextOrValue instanceof BaseParamWithPrefix) {
|
||||
BaseParamWithPrefix<?> paramWithPrefix = (BaseParamWithPrefix<?>) nextOrValue;
|
||||
if (paramWithPrefix.getPrefix() != null) {
|
||||
String message = "Search with params " + theComboParamNames
|
||||
+ " is not a candidate for combo searching - Parameter '" + nextParamName
|
||||
+ "' has prefix: '"
|
||||
+ paramWithPrefix.getPrefix().getValue() + "'";
|
||||
firePerformanceInfo(theRequest, message);
|
||||
paramValuesAreValidForCombo = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isNotBlank(nextOrValue.getQueryParameterQualifier())) {
|
||||
String message = "Search with params " + theComboParamNames
|
||||
+ " is not a candidate for combo searching - Parameter '" + nextParamName
|
||||
+ "' has modifier: '" + nextOrValue.getQueryParameterQualifier() + "'";
|
||||
firePerformanceInfo(theRequest, message);
|
||||
paramValuesAreValidForCombo = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Reference params are only eligible for using a composite index if they
|
||||
|
@ -2039,6 +2089,13 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (CartesianProductUtil.calculateCartesianProductSize(paramOrValues) > 500) {
|
||||
ourLog.debug(
|
||||
"Search is not a candidate for unique combo searching - Too many OR values would result in too many permutations");
|
||||
paramValuesAreValidForCombo = false;
|
||||
}
|
||||
|
||||
return paramValuesAreValidForCombo;
|
||||
}
|
||||
|
||||
|
@ -2416,16 +2473,27 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
|||
}
|
||||
}
|
||||
|
||||
private void firePerformanceInfo(RequestDetails theRequest, String theMessage) {
|
||||
// Only log at debug level since these messages aren't considered important enough
|
||||
// that we should be cluttering the system log, but they are important to the
|
||||
// specific query being executed to we'll INFO level them there
|
||||
ourLog.debug(theMessage);
|
||||
firePerformanceMessage(theRequest, theMessage, Pointcut.JPA_PERFTRACE_INFO);
|
||||
}
|
||||
|
||||
private void firePerformanceWarning(RequestDetails theRequest, String theMessage) {
|
||||
ourLog.warn(theMessage);
|
||||
firePerformanceMessage(theRequest, theMessage, Pointcut.JPA_PERFTRACE_WARNING);
|
||||
}
|
||||
|
||||
private void firePerformanceMessage(RequestDetails theRequest, String theMessage, Pointcut pointcut) {
|
||||
StorageProcessingMessage message = new StorageProcessingMessage();
|
||||
message.setMessage(theMessage);
|
||||
HookParams params = new HookParams()
|
||||
.add(RequestDetails.class, theRequest)
|
||||
.addIfMatchesType(ServletRequestDetails.class, theRequest)
|
||||
.add(StorageProcessingMessage.class, message);
|
||||
CompositeInterceptorBroadcaster.doCallHooks(
|
||||
myInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_WARNING, params);
|
||||
CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, pointcut, params);
|
||||
}
|
||||
|
||||
public static int getMaximumPageSize() {
|
||||
|
|
|
@ -23,10 +23,13 @@ import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
|||
import ca.uhn.fhir.jpa.model.entity.PartitionablePartitionId;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboTokenNonUnique;
|
||||
import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder;
|
||||
import com.healthmarketscience.sqlbuilder.BinaryCondition;
|
||||
import ca.uhn.fhir.jpa.util.QueryParameterUtils;
|
||||
import com.healthmarketscience.sqlbuilder.Condition;
|
||||
import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ComboNonUniqueSearchParameterPredicateBuilder extends BaseSearchParamPredicateBuilder {
|
||||
|
||||
private final DbColumn myColumnHashComplete;
|
||||
|
@ -40,12 +43,16 @@ public class ComboNonUniqueSearchParameterPredicateBuilder extends BaseSearchPar
|
|||
myColumnHashComplete = getTable().addColumn("HASH_COMPLETE");
|
||||
}
|
||||
|
||||
public Condition createPredicateHashComplete(RequestPartitionId theRequestPartitionId, String theIndexString) {
|
||||
public Condition createPredicateHashComplete(
|
||||
RequestPartitionId theRequestPartitionId, List<String> theIndexStrings) {
|
||||
PartitionablePartitionId partitionId =
|
||||
PartitionablePartitionId.toStoragePartition(theRequestPartitionId, getPartitionSettings());
|
||||
long hash = ResourceIndexedComboTokenNonUnique.calculateHashComplete(
|
||||
getPartitionSettings(), partitionId, theIndexString);
|
||||
BinaryCondition predicate = BinaryCondition.equalTo(myColumnHashComplete, generatePlaceholder(hash));
|
||||
List<Long> hashes = theIndexStrings.stream()
|
||||
.map(t -> ResourceIndexedComboTokenNonUnique.calculateHashComplete(
|
||||
getPartitionSettings(), partitionId, t))
|
||||
.collect(Collectors.toList());
|
||||
Condition predicate =
|
||||
QueryParameterUtils.toEqualToOrInPredicate(myColumnHashComplete, generatePlaceholders(hashes));
|
||||
return combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,10 +21,12 @@ package ca.uhn.fhir.jpa.search.builder.predicate;
|
|||
|
||||
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
||||
import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder;
|
||||
import com.healthmarketscience.sqlbuilder.BinaryCondition;
|
||||
import ca.uhn.fhir.jpa.util.QueryParameterUtils;
|
||||
import com.healthmarketscience.sqlbuilder.Condition;
|
||||
import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ComboUniqueSearchParameterPredicateBuilder extends BaseSearchParamPredicateBuilder {
|
||||
|
||||
private final DbColumn myColumnString;
|
||||
|
@ -38,8 +40,10 @@ public class ComboUniqueSearchParameterPredicateBuilder extends BaseSearchParamP
|
|||
myColumnString = getTable().addColumn("IDX_STRING");
|
||||
}
|
||||
|
||||
public Condition createPredicateIndexString(RequestPartitionId theRequestPartitionId, String theIndexString) {
|
||||
BinaryCondition predicate = BinaryCondition.equalTo(myColumnString, generatePlaceholder(theIndexString));
|
||||
public Condition createPredicateIndexString(
|
||||
RequestPartitionId theRequestPartitionId, List<String> theIndexStrings) {
|
||||
Condition predicate =
|
||||
QueryParameterUtils.toEqualToOrInPredicate(myColumnString, generatePlaceholders(theIndexStrings));
|
||||
return combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
package ca.uhn.fhir.jpa.util;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Utility class for working with cartesian products - Use Guava's
|
||||
* {@link com.google.common.collect.Lists#cartesianProduct(List)} method
|
||||
* to actually calculate the product.
|
||||
*/
|
||||
public class CartesianProductUtil {
|
||||
|
||||
/**
|
||||
* Non instantiable
|
||||
*/
|
||||
private CartesianProductUtil() {
|
||||
// nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the cartesian product
|
||||
*
|
||||
* @throws ArithmeticException If size exceeds {@link Integer#MAX_VALUE}
|
||||
* @since 7.4.0
|
||||
*/
|
||||
public static <T> int calculateCartesianProductSize(List<List<T>> theLists) throws ArithmeticException {
|
||||
int retVal = !theLists.isEmpty() ? 1 : 0;
|
||||
for (List<T> theList : theLists) {
|
||||
retVal = Math.multiplyExact(retVal, theList.size());
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package ca.uhn.fhir.jpa.util;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class CartesianProductUtilTest {
|
||||
|
||||
@Test
|
||||
public void testCalculateCartesianProductSize() {
|
||||
List<List<String>> input = List.of(
|
||||
List.of("A0", "A1"),
|
||||
List.of("B0", "B1", "B2"),
|
||||
List.of("C0", "C1")
|
||||
);
|
||||
assertEquals(12, CartesianProductUtil.calculateCartesianProductSize(input));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCalculateCartesianProductSizeEmpty() {
|
||||
List<List<String>> input = List.of();
|
||||
assertEquals(0, CartesianProductUtil.calculateCartesianProductSize(input));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCalculateCartesianProductSizeOverflow() {
|
||||
List<Integer> ranges = IntStream.range(0, 10001).boxed().toList();
|
||||
List<List<Integer>> input = IntStream.range(0, 20).boxed().map(t -> ranges).toList();
|
||||
assertThrows(ArithmeticException.class, () -> CartesianProductUtil.calculateCartesianProductSize(input));
|
||||
}
|
||||
|
||||
}
|
|
@ -353,22 +353,6 @@ public class SearchParameterMap implements Serializable {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* This will only return true if all parameters have no modifier of any kind
|
||||
*/
|
||||
public boolean isAllParametersHaveNoModifier() {
|
||||
for (List<List<IQueryParameterType>> nextParamName : values()) {
|
||||
for (List<IQueryParameterType> nextAnd : nextParamName) {
|
||||
for (IQueryParameterType nextOr : nextAnd) {
|
||||
if (isNotBlank(nextOr.getQueryParameterQualifier())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* If set, tells the server to load these results synchronously, and not to load
|
||||
* more than X results
|
||||
|
|
|
@ -2,26 +2,39 @@ package ca.uhn.fhir.jpa.dao.r4;
|
|||
|
||||
import ca.uhn.fhir.interceptor.api.IInterceptorService;
|
||||
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboTokenNonUnique;
|
||||
import ca.uhn.fhir.jpa.model.entity.StorageSettings;
|
||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.jpa.searchparam.submit.interceptor.SearchParamValidatingInterceptor;
|
||||
import ca.uhn.fhir.jpa.util.SqlQuery;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
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.ReferenceParam;
|
||||
import ca.uhn.fhir.rest.param.StringAndListParam;
|
||||
import ca.uhn.fhir.rest.param.StringOrListParam;
|
||||
import ca.uhn.fhir.rest.param.StringParam;
|
||||
import ca.uhn.fhir.rest.param.TokenAndListParam;
|
||||
import ca.uhn.fhir.rest.param.TokenOrListParam;
|
||||
import ca.uhn.fhir.rest.param.TokenParam;
|
||||
import ca.uhn.fhir.util.HapiExtensions;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.model.BooleanType;
|
||||
import org.hl7.fhir.r4.model.DateTimeType;
|
||||
import org.hl7.fhir.r4.model.DateType;
|
||||
import org.hl7.fhir.r4.model.Enumerations;
|
||||
import org.hl7.fhir.r4.model.Enumerations.PublicationStatus;
|
||||
import org.hl7.fhir.r4.model.Observation;
|
||||
import org.hl7.fhir.r4.model.Organization;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.hl7.fhir.r4.model.SearchParameter;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
@ -48,6 +61,8 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
|||
|
||||
@AfterEach
|
||||
public void restoreInterceptor() {
|
||||
myStorageSettings.setIndexMissingFields(new StorageSettings().getIndexMissingFields());
|
||||
|
||||
if (myInterceptorFound) {
|
||||
myInterceptorService.unregisterInterceptor(mySearchParamValidatingInterceptor);
|
||||
}
|
||||
|
@ -133,14 +148,14 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
|||
List<ResourceIndexedComboTokenNonUnique> indexedTokens = myResourceIndexedComboTokensNonUniqueDao.findAll();
|
||||
indexedTokens.sort(Comparator.comparing(ResourceIndexedComboTokenNonUnique::getId));
|
||||
assertEquals(2, indexedTokens.size());
|
||||
String expected = "Patient?family=FAMILY1%5C%7C&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale&given=GIVEN1";
|
||||
String expected = "Patient?family=FAMILY1&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale&given=GIVEN1";
|
||||
assertEquals(expected, indexedTokens.get(0).getIndexString());
|
||||
assertEquals(-7504889232313729794L, indexedTokens.get(0).getHashComplete().longValue());
|
||||
assertEquals(-2634469377090377342L, indexedTokens.get(0).getHashComplete().longValue());
|
||||
});
|
||||
|
||||
myMessages.clear();
|
||||
SearchParameterMap params = SearchParameterMap.newSynchronous();
|
||||
params.add("family", new StringParam("fAmIlY1|")); // weird casing to test normalization
|
||||
params.add("family", new StringParam("fAmIlY1")); // weird casing to test normalization
|
||||
params.add("given", new StringParam("gIVEn1"));
|
||||
params.add("gender", new TokenParam("http://hl7.org/fhir/administrative-gender", "male"));
|
||||
myCaptureQueriesListener.clear();
|
||||
|
@ -150,11 +165,11 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
|||
assertThat(actual).containsExactlyInAnyOrder(id1.toUnqualifiedVersionless().getValue());
|
||||
|
||||
assertThat(myCaptureQueriesListener.getSelectQueries().stream().map(t -> t.getSql(true, false)).toList()).contains(
|
||||
"SELECT t0.RES_ID FROM HFJ_IDX_CMB_TOK_NU t0 WHERE (t0.HASH_COMPLETE = '-7504889232313729794')"
|
||||
"SELECT t0.RES_ID FROM HFJ_IDX_CMB_TOK_NU t0 WHERE (t0.HASH_COMPLETE = '-2634469377090377342')"
|
||||
);
|
||||
|
||||
logCapturedMessages();
|
||||
assertThat(myMessages.toString()).contains("[INFO Using NON_UNIQUE index for query for search: Patient?family=FAMILY1%5C%7C&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale&given=GIVEN1]");
|
||||
assertThat(myMessages.toString()).contains("[INFO Using NON_UNIQUE index(es) for query for search: Patient?family=FAMILY1&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale&given=GIVEN1]");
|
||||
myMessages.clear();
|
||||
|
||||
// Remove 1, add another
|
||||
|
@ -165,7 +180,7 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
|||
assertNotNull(id3);
|
||||
|
||||
params = SearchParameterMap.newSynchronous();
|
||||
params.add("family", new StringParam("fAmIlY1|")); // weird casing to test normalization
|
||||
params.add("family", new StringParam("fAmIlY1")); // weird casing to test normalization
|
||||
params.add("given", new StringParam("gIVEn1"));
|
||||
params.add("gender", new TokenParam("http://hl7.org/fhir/administrative-gender", "male"));
|
||||
results = myPatientDao.search(params, mySrd);
|
||||
|
@ -175,6 +190,23 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
|||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyParamLists() {
|
||||
createStringAndTokenCombo_NameAndGender();
|
||||
|
||||
IIdType id1 = createPatient1(null);
|
||||
IIdType id2 = createPatient2(null);
|
||||
|
||||
SearchParameterMap params = SearchParameterMap.newSynchronous();
|
||||
params.add("family", new StringAndListParam());
|
||||
params.add("given", new StringAndListParam());
|
||||
params.add("gender", new TokenAndListParam());
|
||||
IBundleProvider results = myPatientDao.search(params, mySrd);
|
||||
List<String> actual = toUnqualifiedVersionlessIdValues(results);
|
||||
assertThat(actual).containsExactlyInAnyOrder(id1.toUnqualifiedVersionless().getValue(), id2.toUnqualifiedVersionless().getValue());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStringAndToken_CreateAndUpdate() {
|
||||
createStringAndTokenCombo_NameAndGender();
|
||||
|
@ -197,7 +229,7 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
|||
.stream()
|
||||
.map(ResourceIndexedComboTokenNonUnique::getIndexString)
|
||||
.toList();
|
||||
assertThat(indexes).as(indexes.toString()).containsExactly("Patient?family=FAMILY1%5C%7C&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale&given=GIVEN1");
|
||||
assertThat(indexes).as(indexes.toString()).containsExactly("Patient?family=FAMILY1&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale&given=GIVEN1");
|
||||
});
|
||||
|
||||
/*
|
||||
|
@ -223,7 +255,7 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
|||
.stream()
|
||||
.map(ResourceIndexedComboTokenNonUnique::getIndexString)
|
||||
.toList();
|
||||
assertThat(indexes).as(indexes.toString()).containsExactly("Patient?family=FAMILY2%5C%7C&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale&given=GIVEN1");
|
||||
assertThat(indexes).as(indexes.toString()).containsExactly("Patient?family=FAMILY2&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale&given=GIVEN1");
|
||||
});
|
||||
|
||||
}
|
||||
|
@ -243,12 +275,12 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
|||
List<ResourceIndexedComboTokenNonUnique> indexedTokens = myResourceIndexedComboTokensNonUniqueDao.findAll();
|
||||
indexedTokens.sort(Comparator.comparing(ResourceIndexedComboTokenNonUnique::getId));
|
||||
assertEquals(2, indexedTokens.size());
|
||||
assertEquals(-7504889232313729794L, indexedTokens.get(0).getHashComplete().longValue());
|
||||
assertEquals(-2634469377090377342L, indexedTokens.get(0).getHashComplete().longValue());
|
||||
});
|
||||
|
||||
myMessages.clear();
|
||||
SearchParameterMap params = SearchParameterMap.newSynchronous();
|
||||
params.add("family", new StringParam("fAmIlY1|")); // weird casing to test normalization
|
||||
params.add("family", new StringParam("fAmIlY1")); // weird casing to test normalization
|
||||
params.add("given", new StringParam("gIVEn1"));
|
||||
params.add("gender", new TokenParam("http://hl7.org/fhir/administrative-gender", "male"));
|
||||
params.add("birthdate", new DateParam("2021-02-02"));
|
||||
|
@ -259,11 +291,43 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
|||
assertThat(actual).containsExactlyInAnyOrder(id1.toUnqualifiedVersionless().getValue());
|
||||
|
||||
String sql = myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, false);
|
||||
String expected = "SELECT t1.RES_ID FROM HFJ_RESOURCE t1 INNER JOIN HFJ_IDX_CMB_TOK_NU t0 ON (t1.RES_ID = t0.RES_ID) INNER JOIN HFJ_SPIDX_DATE t2 ON (t1.RES_ID = t2.RES_ID) WHERE ((t0.HASH_COMPLETE = '-7504889232313729794') AND ((t2.HASH_IDENTITY = '5247847184787287691') AND ((t2.SP_VALUE_LOW_DATE_ORDINAL >= '20210202') AND (t2.SP_VALUE_HIGH_DATE_ORDINAL <= '20210202'))))";
|
||||
String expected = "SELECT t1.RES_ID FROM HFJ_RESOURCE t1 INNER JOIN HFJ_IDX_CMB_TOK_NU t0 ON (t1.RES_ID = t0.RES_ID) INNER JOIN HFJ_SPIDX_DATE t2 ON (t1.RES_ID = t2.RES_ID) WHERE ((t0.HASH_COMPLETE = '-2634469377090377342') AND ((t2.HASH_IDENTITY = '5247847184787287691') AND ((t2.SP_VALUE_LOW_DATE_ORDINAL >= '20210202') AND (t2.SP_VALUE_HIGH_DATE_ORDINAL <= '20210202'))))";
|
||||
assertEquals(expected, sql);
|
||||
|
||||
logCapturedMessages();
|
||||
assertThat(myMessages.toString()).contains("[INFO Using NON_UNIQUE index for query for search: Patient?family=FAMILY1%5C%7C&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale&given=GIVEN1]");
|
||||
assertThat(myMessages.toString()).contains("[INFO Using NON_UNIQUE index(es) for query for search: Patient?family=FAMILY1&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale&given=GIVEN1]");
|
||||
myMessages.clear();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testStringAndToken_MultipleAnd() {
|
||||
createStringAndTokenCombo_NameAndGender();
|
||||
|
||||
IIdType id1 = createPatient(withFamily("SIMPSON"), withGiven("HOMER"), withGiven("JAY"), withGender("male"));
|
||||
assertNotNull(id1);
|
||||
|
||||
logAllNonUniqueIndexes();
|
||||
logAllStringIndexes();
|
||||
|
||||
myMessages.clear();
|
||||
SearchParameterMap params = SearchParameterMap.newSynchronous();
|
||||
params.add("family", new StringParam("Simpson"));
|
||||
params.add("given", new StringAndListParam().addAnd(new StringParam("Homer")).addAnd(new StringParam("Jay")));
|
||||
params.add("gender", new TokenParam("male"));
|
||||
myCaptureQueriesListener.clear();
|
||||
IBundleProvider results = myPatientDao.search(params, mySrd);
|
||||
List<String> actual = toUnqualifiedVersionlessIdValues(results);
|
||||
myCaptureQueriesListener.logSelectQueries();
|
||||
assertThat(actual).containsExactlyInAnyOrder(id1.toUnqualifiedVersionless().getValue());
|
||||
|
||||
String sql = myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, false);
|
||||
String expected = "SELECT t0.RES_ID FROM HFJ_IDX_CMB_TOK_NU t0 INNER JOIN HFJ_SPIDX_STRING t1 ON (t0.RES_ID = t1.RES_ID) WHERE ((t0.HASH_COMPLETE = '7545664593829342272') AND ((t1.HASH_NORM_PREFIX = '6206712800146298788') AND (t1.SP_VALUE_NORMALIZED LIKE 'JAY%')))";
|
||||
assertEquals(expected, sql);
|
||||
|
||||
logCapturedMessages();
|
||||
assertThat(myMessages.toString()).contains("Using NON_UNIQUE index(es) for query for search: Patient?family=SIMPSON&gender=male&given=HOMER");
|
||||
myMessages.clear();
|
||||
|
||||
}
|
||||
|
@ -271,7 +335,7 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
|||
|
||||
@Test
|
||||
public void testStringAndDate_Create() {
|
||||
createStringAndTokenCombo_NameAndBirthdate();
|
||||
createStringAndDateCombo_NameAndBirthdate();
|
||||
|
||||
IIdType id1 = createPatient1(null);
|
||||
assertNotNull(id1);
|
||||
|
@ -303,45 +367,10 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
|||
assertEquals(expected, myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false));
|
||||
|
||||
logCapturedMessages();
|
||||
assertThat(myMessages.toString()).contains("[INFO Using NON_UNIQUE index for query for search: Patient?birthdate=2021-02-02&family=FAMILY1]");
|
||||
assertThat(myMessages.toString()).contains("[INFO Using NON_UNIQUE index(es) for query for search: Patient?birthdate=2021-02-02&family=FAMILY1]");
|
||||
myMessages.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Can't create or search for combo params with partial dates
|
||||
*/
|
||||
@Test
|
||||
public void testStringAndDate_Create_PartialDate() {
|
||||
createStringAndTokenCombo_NameAndBirthdate();
|
||||
|
||||
Patient pt1 = new Patient();
|
||||
pt1.getNameFirstRep().setFamily("Family1").addGiven("Given1");
|
||||
pt1.setBirthDateElement(new DateType("2021-02"));
|
||||
IIdType id1 = myPatientDao.create(pt1, mySrd).getId().toUnqualified();
|
||||
|
||||
logAllNonUniqueIndexes();
|
||||
runInTransaction(() -> {
|
||||
List<ResourceIndexedComboTokenNonUnique> indexedTokens = myResourceIndexedComboTokensNonUniqueDao.findAll();
|
||||
assertEquals(0, indexedTokens.size());
|
||||
});
|
||||
|
||||
myMessages.clear();
|
||||
SearchParameterMap params = SearchParameterMap.newSynchronous();
|
||||
params.add("family", new StringParam("family1"));
|
||||
params.add("birthdate", new DateParam("2021-02"));
|
||||
myCaptureQueriesListener.clear();
|
||||
IBundleProvider results = myPatientDao.search(params, mySrd);
|
||||
List<String> actual = toUnqualifiedVersionlessIdValues(results);
|
||||
myCaptureQueriesListener.logSelectQueries();
|
||||
assertThat(actual).contains(id1.toUnqualifiedVersionless().getValue());
|
||||
|
||||
String sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false);
|
||||
assertThat(sql).doesNotContain("HFJ_IDX_CMB_TOK_NU");
|
||||
|
||||
logCapturedMessages();
|
||||
assertThat(myMessages).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStringAndReference_Create() {
|
||||
createStringAndReferenceCombo_FamilyAndOrganization();
|
||||
|
@ -356,12 +385,12 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
|||
List<ResourceIndexedComboTokenNonUnique> indexedTokens = myResourceIndexedComboTokensNonUniqueDao.findAll();
|
||||
indexedTokens.sort(Comparator.comparing(ResourceIndexedComboTokenNonUnique::getId));
|
||||
assertEquals(2, indexedTokens.size());
|
||||
assertEquals("Patient?family=FAMILY1%5C%7C&organization=Organization%2Fmy-org", indexedTokens.get(0).getIndexString());
|
||||
assertEquals("Patient?family=FAMILY1&organization=Organization%2Fmy-org", indexedTokens.get(0).getIndexString());
|
||||
});
|
||||
|
||||
myMessages.clear();
|
||||
SearchParameterMap params = SearchParameterMap.newSynchronous();
|
||||
params.add("family", new StringParam("fAmIlY1|")); // weird casing to test normalization
|
||||
params.add("family", new StringParam("fAmIlY1")); // weird casing to test normalization
|
||||
params.add("organization", new ReferenceParam(ORG_ID_QUALIFIED));
|
||||
myCaptureQueriesListener.clear();
|
||||
IBundleProvider results = myPatientDao.search(params, mySrd);
|
||||
|
@ -369,7 +398,7 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
|||
myCaptureQueriesListener.logSelectQueries();
|
||||
assertThat(actual).contains(id1.toUnqualifiedVersionless().getValue());
|
||||
|
||||
String expected = "SELECT t0.RES_ID FROM HFJ_IDX_CMB_TOK_NU t0 WHERE (t0.HASH_COMPLETE = '2277801301223576208')";
|
||||
String expected = "SELECT t0.RES_ID FROM HFJ_IDX_CMB_TOK_NU t0 WHERE (t0.HASH_COMPLETE = '2591238402961312979')";
|
||||
assertEquals(expected, myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false));
|
||||
}
|
||||
|
||||
|
@ -403,6 +432,104 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
|||
assertThat(myMessages.get(0)).contains("This search uses an unqualified resource");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* If there are two parameters as a part of a combo param, and we have
|
||||
* multiple AND repetitions for both, then we can just join on the
|
||||
* combo index twice.
|
||||
*/
|
||||
@Test
|
||||
public void testMultipleAndCombinations_EqualNumbers() {
|
||||
createStringAndStringCombo_FamilyAndGiven();
|
||||
|
||||
createPatient(
|
||||
withId("A"),
|
||||
withFamily("SIMPSON"), withGiven("HOMER"),
|
||||
withFamily("jones"), withGiven("frank")
|
||||
);
|
||||
createPatient(
|
||||
withId("B"),
|
||||
withFamily("SIMPSON"), withGiven("MARGE")
|
||||
);
|
||||
|
||||
SearchParameterMap params = SearchParameterMap.newSynchronous();
|
||||
params.add("family", new StringAndListParam().addAnd(new StringParam("simpson")).addAnd(new StringParam("JONES")));
|
||||
params.add("given", new StringAndListParam().addAnd(new StringParam("homer")).addAnd(new StringParam("frank")));
|
||||
myCaptureQueriesListener.clear();
|
||||
IBundleProvider results = myPatientDao.search(params, mySrd);
|
||||
List<String> actual = toUnqualifiedVersionlessIdValues(results);
|
||||
myCaptureQueriesListener.logSelectQueries();
|
||||
assertThat(actual).contains("Patient/A");
|
||||
|
||||
String expected = "SELECT t0.RES_ID FROM HFJ_IDX_CMB_TOK_NU t0 INNER JOIN HFJ_IDX_CMB_TOK_NU t1 ON (t0.RES_ID = t1.RES_ID) WHERE ((t0.HASH_COMPLETE = '822090206952728926') AND (t1.HASH_COMPLETE = '-8088946700286918311'))";
|
||||
assertEquals(expected, myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* If there are two parameters as a part of a combo param, and we have
|
||||
* multiple AND repetitions for one but not the other, than we'll use
|
||||
* the combo index for the first pair, but we don't create a second pair.
|
||||
* We could probably optimize this to use the combo index for both, but
|
||||
* it's not clear that this would actually help and this is probably
|
||||
* a pretty contrived use case.
|
||||
*/
|
||||
@Test
|
||||
public void testMultipleAndCombinations_NonEqualNumbers() {
|
||||
createStringAndStringCombo_FamilyAndGiven();
|
||||
|
||||
createPatient(
|
||||
withId("A"),
|
||||
withFamily("SIMPSON"), withGiven("HOMER"),
|
||||
withFamily("jones"), withGiven("frank")
|
||||
);
|
||||
createPatient(
|
||||
withId("B"),
|
||||
withFamily("SIMPSON"), withGiven("MARGE")
|
||||
);
|
||||
|
||||
SearchParameterMap params = SearchParameterMap.newSynchronous();
|
||||
params.add("family", new StringAndListParam().addAnd(new StringParam("simpson")).addAnd(new StringParam("JONES")));
|
||||
params.add("given", new StringAndListParam().addAnd(new StringParam("homer")));
|
||||
myCaptureQueriesListener.clear();
|
||||
IBundleProvider results = myPatientDao.search(params, mySrd);
|
||||
List<String> actual = toUnqualifiedVersionlessIdValues(results);
|
||||
myCaptureQueriesListener.logSelectQueries();
|
||||
assertThat(actual).contains("Patient/A");
|
||||
|
||||
String expected = "SELECT t0.RES_ID FROM HFJ_IDX_CMB_TOK_NU t0 INNER JOIN HFJ_SPIDX_STRING t1 ON (t0.RES_ID = t1.RES_ID) WHERE ((t0.HASH_COMPLETE = '822090206952728926') AND ((t1.HASH_NORM_PREFIX = '-3664262414674370905') AND (t1.SP_VALUE_NORMALIZED LIKE 'JONES%')))";
|
||||
assertEquals(expected, myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false));
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testOrQuery() {
|
||||
createTokenAndReferenceCombo_FamilyAndOrganization();
|
||||
|
||||
createPatient(withId("PAT"), withActiveTrue());
|
||||
createObservation(withId("O1"), withSubject("Patient/PAT"), withStatus("final"));
|
||||
createObservation(withId("O2"), withSubject("Patient/PAT"), withStatus("registered"));
|
||||
|
||||
SearchParameterMap params = SearchParameterMap.newSynchronous();
|
||||
params.add("patient", new ReferenceParam("Patient/PAT"));
|
||||
params.add("status", new TokenOrListParam(null, "preliminary", "final", "amended"));
|
||||
myCaptureQueriesListener.clear();
|
||||
IBundleProvider results = myObservationDao.search(params, mySrd);
|
||||
List<String> actual = toUnqualifiedVersionlessIdValues(results);
|
||||
myCaptureQueriesListener.logSelectQueries();
|
||||
assertThat(actual).contains("Observation/O1");
|
||||
|
||||
String expected = "SELECT t0.RES_ID FROM HFJ_IDX_CMB_TOK_NU t0 WHERE (t0.HASH_COMPLETE IN ('2445648980345828396','-6884698528022589694','-8034948665712960724') )";
|
||||
assertEquals(expected, myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false));
|
||||
|
||||
logCapturedMessages();
|
||||
assertThat(myMessages.toString()).contains("Observation?patient=Patient%2FPAT&status=amended", "Observation?patient=Patient%2FPAT&status=final", "Observation?patient=Patient%2FPAT&status=preliminary");
|
||||
myMessages.clear();
|
||||
|
||||
}
|
||||
|
||||
|
||||
private void createOrg() {
|
||||
Organization org = new Organization();
|
||||
org.setName("Some Org");
|
||||
|
@ -410,7 +537,7 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
|||
myOrganizationDao.update(org, mySrd);
|
||||
}
|
||||
|
||||
private void createStringAndTokenCombo_NameAndBirthdate() {
|
||||
private void createStringAndDateCombo_NameAndBirthdate() {
|
||||
SearchParameter sp = new SearchParameter();
|
||||
sp.setId("SearchParameter/patient-family");
|
||||
sp.setType(Enumerations.SearchParamType.STRING);
|
||||
|
@ -450,12 +577,52 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
|||
myMessages.clear();
|
||||
}
|
||||
|
||||
private void createStringAndStringCombo_FamilyAndGiven() {
|
||||
SearchParameter sp = new SearchParameter();
|
||||
sp.setId("SearchParameter/patient-family");
|
||||
sp.setType(Enumerations.SearchParamType.STRING);
|
||||
sp.setCode("family");
|
||||
sp.setExpression("Patient.name.family");
|
||||
sp.setStatus(PublicationStatus.ACTIVE);
|
||||
sp.addBase("Patient");
|
||||
mySearchParameterDao.update(sp, mySrd);
|
||||
|
||||
sp = new SearchParameter();
|
||||
sp.setId("SearchParameter/patient-given");
|
||||
sp.setType(Enumerations.SearchParamType.STRING);
|
||||
sp.setCode("given");
|
||||
sp.setExpression("Patient.name.given");
|
||||
sp.setStatus(PublicationStatus.ACTIVE);
|
||||
sp.addBase("Patient");
|
||||
mySearchParameterDao.update(sp, mySrd);
|
||||
|
||||
sp = new SearchParameter();
|
||||
sp.setId("SearchParameter/patient-names");
|
||||
sp.setType(Enumerations.SearchParamType.COMPOSITE);
|
||||
sp.setStatus(PublicationStatus.ACTIVE);
|
||||
sp.addBase("Patient");
|
||||
sp.addComponent()
|
||||
.setExpression("Patient")
|
||||
.setDefinition("SearchParameter/patient-family");
|
||||
sp.addComponent()
|
||||
.setExpression("Patient")
|
||||
.setDefinition("SearchParameter/patient-given");
|
||||
sp.addExtension()
|
||||
.setUrl(HapiExtensions.EXT_SP_UNIQUE)
|
||||
.setValue(new BooleanType(false));
|
||||
mySearchParameterDao.update(sp, mySrd);
|
||||
|
||||
mySearchParamRegistry.forceRefresh();
|
||||
|
||||
myMessages.clear();
|
||||
}
|
||||
|
||||
private void createStringAndTokenCombo_NameAndGender() {
|
||||
SearchParameter sp = new SearchParameter();
|
||||
sp.setId("SearchParameter/patient-family");
|
||||
sp.setType(Enumerations.SearchParamType.STRING);
|
||||
sp.setCode("family");
|
||||
sp.setExpression("Patient.name.family + '|'");
|
||||
sp.setExpression("Patient.name.family");
|
||||
sp.setStatus(PublicationStatus.ACTIVE);
|
||||
sp.addBase("Patient");
|
||||
mySearchParameterDao.update(sp, mySrd);
|
||||
|
@ -507,7 +674,7 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
|||
sp.setId("SearchParameter/patient-family");
|
||||
sp.setType(Enumerations.SearchParamType.STRING);
|
||||
sp.setCode("family");
|
||||
sp.setExpression("Patient.name.family + '|'");
|
||||
sp.setExpression("Patient.name.family");
|
||||
sp.setStatus(PublicationStatus.ACTIVE);
|
||||
sp.addBase("Patient");
|
||||
mySearchParameterDao.update(sp, mySrd);
|
||||
|
@ -542,6 +709,45 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
|||
myMessages.clear();
|
||||
}
|
||||
|
||||
private void createTokenAndReferenceCombo_FamilyAndOrganization() {
|
||||
SearchParameter sp = new SearchParameter();
|
||||
sp.setId("SearchParameter/Observation-status");
|
||||
sp.setType(Enumerations.SearchParamType.TOKEN);
|
||||
sp.setCode(Observation.SP_STATUS);
|
||||
sp.setExpression("Observation.status");
|
||||
sp.setStatus(PublicationStatus.ACTIVE);
|
||||
sp.addBase("Observation");
|
||||
mySearchParameterDao.update(sp, mySrd);
|
||||
|
||||
sp = new SearchParameter();
|
||||
sp.setId("SearchParameter/clinical-patient");
|
||||
sp.setType(Enumerations.SearchParamType.REFERENCE);
|
||||
sp.setCode(Observation.SP_PATIENT);
|
||||
sp.setExpression("Observation.subject.where(resolve() is Patient)");
|
||||
sp.setStatus(PublicationStatus.ACTIVE);
|
||||
sp.addBase("Observation");
|
||||
mySearchParameterDao.update(sp, mySrd);
|
||||
|
||||
sp = new SearchParameter();
|
||||
sp.setId("SearchParameter/observation-status-and-patient");
|
||||
sp.setType(Enumerations.SearchParamType.COMPOSITE);
|
||||
sp.setStatus(PublicationStatus.ACTIVE);
|
||||
sp.addBase("Observation");
|
||||
sp.addComponent()
|
||||
.setExpression("Observation")
|
||||
.setDefinition("SearchParameter/Observation-status");
|
||||
sp.addComponent()
|
||||
.setExpression("Observation")
|
||||
.setDefinition("SearchParameter/clinical-patient");
|
||||
sp.addExtension()
|
||||
.setUrl(HapiExtensions.EXT_SP_UNIQUE)
|
||||
.setValue(new BooleanType(false));
|
||||
mySearchParameterDao.update(sp, mySrd);
|
||||
|
||||
mySearchParamRegistry.forceRefresh();
|
||||
|
||||
myMessages.clear();
|
||||
}
|
||||
|
||||
private IIdType createPatient1(String theOrgId) {
|
||||
Patient pt1 = new Patient();
|
||||
|
@ -561,5 +767,223 @@ public class FhirResourceDaoR4ComboNonUniqueParamTest extends BaseComboParamsR4T
|
|||
return myPatientDao.create(pt2, mySrd).getId().toUnqualified();
|
||||
}
|
||||
|
||||
/**
|
||||
* A few tests where the combo index should not be used
|
||||
*/
|
||||
@Nested
|
||||
public class ShouldNotUseComboIndexTest {
|
||||
|
||||
@BeforeEach
|
||||
public void before() {
|
||||
SearchParameter sp = new SearchParameter();
|
||||
sp.setId("SearchParameter/Observation-date");
|
||||
sp.setType(Enumerations.SearchParamType.DATE);
|
||||
sp.setCode("date");
|
||||
sp.setExpression("Observation.effective");
|
||||
sp.setStatus(PublicationStatus.ACTIVE);
|
||||
sp.addBase("Observation");
|
||||
mySearchParameterDao.update(sp, mySrd);
|
||||
|
||||
sp = new SearchParameter();
|
||||
sp.setId("SearchParameter/Observation-note-text");
|
||||
sp.setType(Enumerations.SearchParamType.STRING);
|
||||
sp.setCode("note-text");
|
||||
sp.setExpression("Observation.note.text");
|
||||
sp.setStatus(PublicationStatus.ACTIVE);
|
||||
sp.addBase("Observation");
|
||||
mySearchParameterDao.update(sp, mySrd);
|
||||
|
||||
sp = new SearchParameter();
|
||||
sp.setId("SearchParameter/observation-date-and-note-text");
|
||||
sp.setType(Enumerations.SearchParamType.COMPOSITE);
|
||||
sp.setStatus(PublicationStatus.ACTIVE);
|
||||
sp.addBase("Observation");
|
||||
sp.addComponent()
|
||||
.setExpression("Observation")
|
||||
.setDefinition("SearchParameter/Observation-date");
|
||||
sp.addComponent()
|
||||
.setExpression("Observation")
|
||||
.setDefinition("SearchParameter/Observation-note-text");
|
||||
sp.addExtension()
|
||||
.setUrl(HapiExtensions.EXT_SP_UNIQUE)
|
||||
.setValue(new BooleanType(false));
|
||||
mySearchParameterDao.update(sp, mySrd);
|
||||
|
||||
mySearchParamRegistry.forceRefresh();
|
||||
|
||||
myMessages.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTooManyPermutations() {
|
||||
// Test
|
||||
StringOrListParam noteTextParam = new StringOrListParam();
|
||||
DateOrListParam dateParam = new DateOrListParam();
|
||||
for (int i = 0; i < 30; i++) {
|
||||
noteTextParam.add(new StringParam("A" + i));
|
||||
noteTextParam.add(new StringParam("B" + i));
|
||||
noteTextParam.add(new StringParam("C" + i));
|
||||
noteTextParam.add(new StringParam("D" + i));
|
||||
dateParam.add(new DateParam("2020-01-" + String.format("%02d", i+1)));
|
||||
}
|
||||
|
||||
SearchParameterMap params = SearchParameterMap
|
||||
.newSynchronous()
|
||||
.add("note-text", noteTextParam)
|
||||
.add("date", dateParam);
|
||||
myCaptureQueriesListener.clear();
|
||||
IBundleProvider results = myObservationDao.search(params, mySrd);
|
||||
assertThat(toUnqualifiedIdValues(results)).isEmpty();
|
||||
|
||||
// Verify
|
||||
myCaptureQueriesListener.logSelectQueries();
|
||||
String formatted = myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, true);
|
||||
assertThat(formatted).doesNotContain("HFJ_IDX_CMB_TOK_NU");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Can't create or search for combo params with dateTimes that don't have DAY precision
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@CsvSource(value = {
|
||||
"2021-01-02T12:00:01.000Z , false",
|
||||
"2021-01-02T12:00:01Z , false",
|
||||
"2021-01-02 , true", // <-- DAY precision
|
||||
"2021-01 , false",
|
||||
"2021 , false",
|
||||
})
|
||||
public void testPartialDateTime(String theDateValue, boolean theShouldUseComboIndex) {
|
||||
IIdType id1 = createObservation(theDateValue);
|
||||
|
||||
logAllNonUniqueIndexes();
|
||||
runInTransaction(() -> {
|
||||
List<ResourceIndexedComboTokenNonUnique> indexedTokens = myResourceIndexedComboTokensNonUniqueDao.findAll();
|
||||
if (theShouldUseComboIndex) {
|
||||
assertEquals(1, indexedTokens.size());
|
||||
} else {
|
||||
assertEquals(0, indexedTokens.size());
|
||||
}
|
||||
});
|
||||
|
||||
SearchParameterMap params = SearchParameterMap
|
||||
.newSynchronous()
|
||||
.add("note-text", new StringParam("Hello"))
|
||||
.add("date", new DateParam(theDateValue));
|
||||
myCaptureQueriesListener.clear();
|
||||
IBundleProvider results = myObservationDao.search(params, mySrd);
|
||||
List<String> actual = toUnqualifiedVersionlessIdValues(results);
|
||||
myCaptureQueriesListener.logSelectQueries();
|
||||
assertThat(actual).contains(id1.toUnqualifiedVersionless().getValue());
|
||||
|
||||
if (theShouldUseComboIndex) {
|
||||
assertComboIndexUsed();
|
||||
} else {
|
||||
assertComboIndexNotUsed();
|
||||
assertThat(myMessages.toString()).contains("INFO Search with params [date, note-text] is not a candidate for combo searching - Date search with non-DAY precision for parameter 'date'");
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {true, false})
|
||||
public void testDateModifier(boolean theUseComparator) {
|
||||
IIdType id1 = createObservation("2021-01-02");
|
||||
createObservation("2023-01-02");
|
||||
|
||||
SearchParameterMap params = SearchParameterMap
|
||||
.newSynchronous()
|
||||
.add("note-text", new StringParam("Hello"))
|
||||
.add("date", new DateParam("2021-01-02").setPrefix(theUseComparator ? ParamPrefixEnum.LESSTHAN_OR_EQUALS : null));
|
||||
myCaptureQueriesListener.clear();
|
||||
IBundleProvider results = myObservationDao.search(params, mySrd);
|
||||
List<String> actual = toUnqualifiedVersionlessIdValues(results);
|
||||
myCaptureQueriesListener.logSelectQueries();
|
||||
assertThat(actual).contains(id1.toUnqualifiedVersionless().getValue());
|
||||
|
||||
if (theUseComparator) {
|
||||
assertComboIndexNotUsed();
|
||||
assertThat(myMessages.toString()).contains("INFO Search with params [date, note-text] is not a candidate for combo searching - Parameter 'date' has prefix: 'le'");
|
||||
} else {
|
||||
assertComboIndexUsed();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {true, false})
|
||||
public void testStringModifier(boolean theUseExact) {
|
||||
IIdType id1 = createObservation("2021-01-02");
|
||||
createObservation("2023-01-02");
|
||||
|
||||
SearchParameterMap params = SearchParameterMap
|
||||
.newSynchronous()
|
||||
.add("note-text", new StringParam("Hello").setExact(theUseExact))
|
||||
.add("date", new DateParam("2021-01-02"));
|
||||
myCaptureQueriesListener.clear();
|
||||
IBundleProvider results = myObservationDao.search(params, mySrd);
|
||||
List<String> actual = toUnqualifiedVersionlessIdValues(results);
|
||||
myCaptureQueriesListener.logSelectQueries();
|
||||
assertThat(actual).contains(id1.toUnqualifiedVersionless().getValue());
|
||||
|
||||
if (theUseExact) {
|
||||
assertComboIndexNotUsed();
|
||||
assertThat(myMessages.toString()).contains("INFO Search with params [date, note-text] is not a candidate for combo searching - Parameter 'note-text' has modifier: ':exact'");
|
||||
} else {
|
||||
assertComboIndexUsed();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {true, false})
|
||||
public void testMissing(boolean theUseMissing) {
|
||||
myStorageSettings.setIndexMissingFields(StorageSettings.IndexEnabledEnum.ENABLED);
|
||||
|
||||
IIdType id1 = createObservation("2021-01-02");
|
||||
createObservation("2023-01-02");
|
||||
|
||||
SearchParameterMap params = SearchParameterMap
|
||||
.newSynchronous()
|
||||
.add("note-text", new StringParam("Hello").setMissing(theUseMissing ? false : null))
|
||||
.add("date", new DateParam("2021-01-02"));
|
||||
myCaptureQueriesListener.clear();
|
||||
IBundleProvider results = myObservationDao.search(params, mySrd);
|
||||
List<String> actual = toUnqualifiedVersionlessIdValues(results);
|
||||
myCaptureQueriesListener.logSelectQueries();
|
||||
assertThat(actual).contains(id1.toUnqualifiedVersionless().getValue());
|
||||
|
||||
if (theUseMissing) {
|
||||
assertComboIndexNotUsed();
|
||||
assertThat(myMessages.toString()).contains("INFO Search with params [date, note-text] is not a candidate for combo searching - Parameter 'note-text' has modifier: ':missing'");
|
||||
} else {
|
||||
assertComboIndexUsed();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void assertComboIndexUsed() {
|
||||
String sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false);
|
||||
assertThat(sql).contains("HFJ_IDX_CMB_TOK_NU");
|
||||
|
||||
assertThat(myMessages.toString()).contains("Using NON_UNIQUE index(es) for query");
|
||||
}
|
||||
|
||||
private void assertComboIndexNotUsed() {
|
||||
String sql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false);
|
||||
assertThat(sql).doesNotContain("HFJ_IDX_CMB_TOK_NU");
|
||||
|
||||
assertThat(myMessages.toString()).doesNotContain("Using NON_UNIQUE index(es) for query");
|
||||
}
|
||||
|
||||
private IIdType createObservation(String theDateValue) {
|
||||
Observation pt1 = new Observation();
|
||||
pt1.addNote().setText("Hello");
|
||||
pt1.setEffective(new DateTimeType(theDateValue));
|
||||
IIdType id1 = myObservationDao.create(pt1, mySrd).getId().toUnqualified();
|
||||
return id1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -13,6 +13,8 @@ import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
|||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
import ca.uhn.fhir.rest.param.DateAndListParam;
|
||||
import ca.uhn.fhir.rest.param.DateOrListParam;
|
||||
import ca.uhn.fhir.rest.param.DateParam;
|
||||
import ca.uhn.fhir.rest.param.ReferenceParam;
|
||||
import ca.uhn.fhir.rest.param.TokenAndListParam;
|
||||
|
@ -21,6 +23,7 @@ import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
|
|||
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
||||
import ca.uhn.fhir.util.HapiExtensions;
|
||||
import jakarta.annotation.Nonnull;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.model.BooleanType;
|
||||
import org.hl7.fhir.r4.model.Bundle;
|
||||
|
@ -60,9 +63,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
|||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
|
||||
public class FhirResourceDaoR4ComboUniqueParamIT extends BaseComboParamsR4Test {
|
||||
public class FhirResourceDaoR4ComboUniqueParamTest extends BaseComboParamsR4Test {
|
||||
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4ComboUniqueParamIT.class);
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4ComboUniqueParamTest.class);
|
||||
|
||||
@Autowired
|
||||
private IJobCoordinator myJobCoordinator;
|
||||
|
@ -382,9 +385,7 @@ public class FhirResourceDaoR4ComboUniqueParamIT extends BaseComboParamsR4Test {
|
|||
IIdType id = myPatientDao.create(pt, mySrd).getId().toUnqualifiedVersionless();
|
||||
myCaptureQueriesListener.logInsertQueries();
|
||||
|
||||
List<ResourceIndexedComboStringUnique> values = runInTransaction(()->{
|
||||
return myResourceIndexedComboStringUniqueDao.findAllForResourceIdForUnitTest(id.getIdPartAsLong());
|
||||
});
|
||||
List<ResourceIndexedComboStringUnique> values = runInTransaction(()-> myResourceIndexedComboStringUniqueDao.findAllForResourceIdForUnitTest(id.getIdPartAsLong()));
|
||||
assertEquals(2, values.size());
|
||||
values.sort(Comparator.comparing(ResourceIndexedComboStringUnique::getIndexString));
|
||||
assertEquals("Patient?identifier=urn%7C111", values.get(0).getIndexString());
|
||||
|
@ -417,7 +418,114 @@ public class FhirResourceDaoR4ComboUniqueParamIT extends BaseComboParamsR4Test {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testDoubleMatchingOnAnd_Search() {
|
||||
public void testDoubleMatchingOnAnd_Search_TwoAndValues() {
|
||||
Pair<String, String> ids = prepareDoubleMatchingSearchParameterAndPatient();
|
||||
String id1 = ids.getLeft();
|
||||
|
||||
// Two AND values
|
||||
myCaptureQueriesListener.clear();
|
||||
SearchParameterMap sp = new SearchParameterMap();
|
||||
sp.setLoadSynchronous(true);
|
||||
sp.add("identifier",
|
||||
new TokenAndListParam()
|
||||
.addAnd(new TokenParam("urn", "111"))
|
||||
.addAnd(new TokenParam("urn", "222"))
|
||||
);
|
||||
IBundleProvider outcome = myPatientDao.search(sp, mySrd);
|
||||
myCaptureQueriesListener.logFirstSelectQueryForCurrentThread();
|
||||
String unformattedSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false);
|
||||
assertThat(unformattedSql).containsSubsequence(
|
||||
"IDX_STRING = 'Patient?identifier=urn%7C111'",
|
||||
"IDX_STRING = 'Patient?identifier=urn%7C222'"
|
||||
);
|
||||
assertThat(unformattedSql).doesNotContain(("HFJ_SPIDX_TOKEN"));
|
||||
assertThat(unformattedSql).doesNotContain(("RES_DELETED_AT"));
|
||||
assertThat(unformattedSql).doesNotContain(("RES_TYPE"));
|
||||
assertThat(toUnqualifiedVersionlessIdValues(outcome)).containsExactlyInAnyOrder(id1);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testDoubleMatchingOnAnd_Search_TwoOrValues() {
|
||||
Pair<String, String> ids = prepareDoubleMatchingSearchParameterAndPatient();
|
||||
String id1 = ids.getLeft();
|
||||
|
||||
// Two OR values on the same resource - Currently composite SPs don't work for this
|
||||
myCaptureQueriesListener.clear();
|
||||
SearchParameterMap sp = new SearchParameterMap();
|
||||
sp.setLoadSynchronous(true);
|
||||
sp.add("identifier",
|
||||
new TokenAndListParam()
|
||||
.addAnd(new TokenParam("urn", "111"), new TokenParam("urn", "222"))
|
||||
);
|
||||
IBundleProvider outcome = myPatientDao.search(sp, mySrd);
|
||||
myCaptureQueriesListener.logFirstSelectQueryForCurrentThread();
|
||||
assertThat(toUnqualifiedVersionlessIdValues(outcome)).containsExactlyInAnyOrder(id1);
|
||||
String unformattedSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false);
|
||||
assertEquals("SELECT t0.RES_ID FROM HFJ_IDX_CMP_STRING_UNIQ t0 WHERE (t0.IDX_STRING IN ('Patient?identifier=urn%7C111','Patient?identifier=urn%7C222') )", unformattedSql);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testDoubleMatchingOnAnd_Search_TwoAndOrValues() {
|
||||
myStorageSettings.setUniqueIndexesCheckedBeforeSave(false);
|
||||
|
||||
createUniqueBirthdateAndGenderSps();
|
||||
|
||||
Patient pt1 = new Patient();
|
||||
pt1.setGender(Enumerations.AdministrativeGender.MALE);
|
||||
pt1.setBirthDateElement(new DateType("2011-01-01"));
|
||||
String id1 = myPatientDao.create(pt1, mySrd).getId().toUnqualifiedVersionless().getValue();
|
||||
|
||||
// Two OR values on the same resource - Currently composite SPs don't work for this
|
||||
myCaptureQueriesListener.clear();
|
||||
SearchParameterMap sp = new SearchParameterMap();
|
||||
sp.setLoadSynchronous(true);
|
||||
sp.add(Patient.SP_GENDER,
|
||||
new TokenAndListParam()
|
||||
.addAnd(new TokenParam("http://hl7.org/fhir/administrative-gender","male"), new TokenParam( "http://hl7.org/fhir/administrative-gender","female"))
|
||||
);
|
||||
sp.add(Patient.SP_BIRTHDATE,
|
||||
new DateAndListParam()
|
||||
.addAnd(new DateParam("2011-01-01"), new DateParam( "2011-02-02"))
|
||||
);
|
||||
IBundleProvider outcome = myPatientDao.search(sp, mySrd);
|
||||
myCaptureQueriesListener.logFirstSelectQueryForCurrentThread();
|
||||
assertThat(toUnqualifiedVersionlessIdValues(outcome)).containsExactlyInAnyOrder(id1);
|
||||
String unformattedSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false);
|
||||
assertEquals("SELECT t0.RES_ID FROM HFJ_IDX_CMP_STRING_UNIQ t0 WHERE (t0.IDX_STRING IN ('Patient?birthdate=2011-01-01&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cfemale','Patient?birthdate=2011-01-01&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale','Patient?birthdate=2011-02-02&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cfemale','Patient?birthdate=2011-02-02&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale') )", unformattedSql);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
public void testDoubleMatchingOnAnd_Search_NonMatching() {
|
||||
Pair<String, String> ids = prepareDoubleMatchingSearchParameterAndPatient();
|
||||
String id1 = ids.getLeft();
|
||||
String id2 = ids.getRight();
|
||||
|
||||
String unformattedSql;
|
||||
|
||||
// Not matching the composite SP at all
|
||||
myCaptureQueriesListener.clear();
|
||||
SearchParameterMap sp = new SearchParameterMap();
|
||||
sp.setLoadSynchronous(true);
|
||||
sp.add("active",
|
||||
new TokenAndListParam()
|
||||
.addAnd(new TokenParam(null, "true"))
|
||||
);
|
||||
IBundleProvider outcome = myPatientDao.search(sp, mySrd);
|
||||
myCaptureQueriesListener.logFirstSelectQueryForCurrentThread();
|
||||
assertThat(toUnqualifiedVersionlessIdValues(outcome)).containsExactlyInAnyOrder(id1, id2);
|
||||
unformattedSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false);
|
||||
assertThat(unformattedSql).doesNotContain(("IDX_STRING"));
|
||||
assertThat(unformattedSql).doesNotContain(("RES_DELETED_AT"));
|
||||
assertThat(unformattedSql).doesNotContain(("RES_TYPE"));
|
||||
|
||||
}
|
||||
|
||||
private Pair<String, String> prepareDoubleMatchingSearchParameterAndPatient() {
|
||||
myStorageSettings.setAdvancedHSearchIndexing(false);
|
||||
createUniqueIndexPatientIdentifier();
|
||||
|
||||
|
@ -437,61 +545,7 @@ public class FhirResourceDaoR4ComboUniqueParamIT extends BaseComboParamsR4Test {
|
|||
pt.addIdentifier().setSystem("urn").setValue("444");
|
||||
myPatientDao.create(pt, mySrd);
|
||||
|
||||
String unformattedSql;
|
||||
|
||||
// Two AND values
|
||||
myCaptureQueriesListener.clear();
|
||||
SearchParameterMap sp = new SearchParameterMap();
|
||||
sp.setLoadSynchronous(true);
|
||||
sp.add("identifier",
|
||||
new TokenAndListParam()
|
||||
.addAnd(new TokenParam("urn", "111"))
|
||||
.addAnd(new TokenParam("urn", "222"))
|
||||
);
|
||||
IBundleProvider outcome = myPatientDao.search(sp, mySrd);
|
||||
myCaptureQueriesListener.logFirstSelectQueryForCurrentThread();
|
||||
unformattedSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false);
|
||||
assertThat(unformattedSql).containsSubsequence(
|
||||
"IDX_STRING = 'Patient?identifier=urn%7C111'",
|
||||
"HASH_SYS_AND_VALUE = '-3122824860083758210'"
|
||||
);
|
||||
assertThat(unformattedSql).doesNotContain(("RES_DELETED_AT"));
|
||||
assertThat(unformattedSql).doesNotContain(("RES_TYPE"));
|
||||
assertThat(toUnqualifiedVersionlessIdValues(outcome)).containsExactlyInAnyOrder(id1);
|
||||
|
||||
// Two OR values on the same resource - Currently composite SPs don't work for this
|
||||
myCaptureQueriesListener.clear();
|
||||
sp = new SearchParameterMap();
|
||||
sp.setLoadSynchronous(true);
|
||||
sp.add("identifier",
|
||||
new TokenAndListParam()
|
||||
.addAnd(new TokenParam("urn", "111"), new TokenParam("urn", "222"))
|
||||
);
|
||||
outcome = myPatientDao.search(sp, mySrd);
|
||||
myCaptureQueriesListener.logFirstSelectQueryForCurrentThread();
|
||||
assertThat(toUnqualifiedVersionlessIdValues(outcome)).containsExactlyInAnyOrder(id1);
|
||||
unformattedSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false);
|
||||
assertThat(unformattedSql).contains("HASH_SYS_AND_VALUE IN ('4101160957635429999','-3122824860083758210')");
|
||||
assertThat(unformattedSql).doesNotContain(("IDX_STRING"));
|
||||
assertThat(unformattedSql).doesNotContain(("RES_DELETED_AT"));
|
||||
assertThat(unformattedSql).doesNotContain(("RES_TYPE"));
|
||||
|
||||
// Not matching the composite SP at all
|
||||
myCaptureQueriesListener.clear();
|
||||
sp = new SearchParameterMap();
|
||||
sp.setLoadSynchronous(true);
|
||||
sp.add("active",
|
||||
new TokenAndListParam()
|
||||
.addAnd(new TokenParam(null, "true"))
|
||||
);
|
||||
outcome = myPatientDao.search(sp, mySrd);
|
||||
myCaptureQueriesListener.logFirstSelectQueryForCurrentThread();
|
||||
assertThat(toUnqualifiedVersionlessIdValues(outcome)).containsExactlyInAnyOrder(id1, id2);
|
||||
unformattedSql = myCaptureQueriesListener.getSelectQueriesForCurrentThread().get(0).getSql(true, false);
|
||||
assertThat(unformattedSql).doesNotContain(("IDX_STRING"));
|
||||
assertThat(unformattedSql).doesNotContain(("RES_DELETED_AT"));
|
||||
assertThat(unformattedSql).doesNotContain(("RES_TYPE"));
|
||||
|
||||
return Pair.of(id1, id2);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -1110,6 +1164,40 @@ public class FhirResourceDaoR4ComboUniqueParamIT extends BaseComboParamsR4Test {
|
|||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOrQuery() {
|
||||
myStorageSettings.setAdvancedHSearchIndexing(false);
|
||||
createUniqueBirthdateAndGenderSps();
|
||||
|
||||
Patient pt1 = new Patient();
|
||||
pt1.setGender(Enumerations.AdministrativeGender.MALE);
|
||||
pt1.setBirthDateElement(new DateType("2011-01-01"));
|
||||
IIdType id1 = myPatientDao.create(pt1, mySrd).getId().toUnqualifiedVersionless();
|
||||
|
||||
Patient pt2 = new Patient();
|
||||
pt2.setGender(Enumerations.AdministrativeGender.MALE);
|
||||
pt2.setBirthDateElement(new DateType("2011-01-02"));
|
||||
IIdType id2 = myPatientDao.create(pt2, mySrd).getId().toUnqualifiedVersionless();
|
||||
|
||||
myCaptureQueriesListener.clear();
|
||||
myMessages.clear();
|
||||
SearchParameterMap params = new SearchParameterMap();
|
||||
params.setLoadSynchronousUpTo(100);
|
||||
params.add("gender", new TokenParam("http://hl7.org/fhir/administrative-gender", "male"));
|
||||
params.add("birthdate", new DateOrListParam().addOr(new DateParam("2011-01-01")).addOr(new DateParam("2011-01-02")));
|
||||
myCaptureQueriesListener.clear();
|
||||
IBundleProvider results = myPatientDao.search(params, mySrd);
|
||||
myCaptureQueriesListener.logFirstSelectQueryForCurrentThread();
|
||||
assertThat(toUnqualifiedVersionlessIdValues(results)).containsExactlyInAnyOrder(id1.getValue(), id2.getValue());
|
||||
|
||||
assertThat(myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, false))
|
||||
.contains("SELECT t0.RES_ID FROM HFJ_IDX_CMP_STRING_UNIQ t0 WHERE (t0.IDX_STRING IN ('Patient?birthdate=2011-01-01&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale','Patient?birthdate=2011-01-02&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale') )");
|
||||
logCapturedMessages();
|
||||
assertThat(myMessages.toString()).contains("Using UNIQUE index(es) for query for search: [Patient?birthdate=2011-01-01&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale, Patient?birthdate=2011-01-02&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale]");
|
||||
myMessages.clear();
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearchSynchronousUsingUniqueComposite() {
|
||||
myStorageSettings.setAdvancedHSearchIndexing(false);
|
||||
|
@ -1136,7 +1224,7 @@ public class FhirResourceDaoR4ComboUniqueParamIT extends BaseComboParamsR4Test {
|
|||
assertThat(toUnqualifiedVersionlessIdValues(results)).containsExactlyInAnyOrder(id1.getValue());
|
||||
|
||||
logCapturedMessages();
|
||||
assertThat(myMessages.toString()).contains("Using UNIQUE index for query for search: Patient?birthdate=2011-01-01&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale");
|
||||
assertThat(myMessages.toString()).contains("Using UNIQUE index(es) for query for search: Patient?birthdate=2011-01-01&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale");
|
||||
myMessages.clear();
|
||||
|
||||
}
|
||||
|
@ -1164,7 +1252,7 @@ public class FhirResourceDaoR4ComboUniqueParamIT extends BaseComboParamsR4Test {
|
|||
String searchId = results.getUuid();
|
||||
assertThat(toUnqualifiedVersionlessIdValues(results)).containsExactlyInAnyOrder(id1);
|
||||
logCapturedMessages();
|
||||
assertThat(myMessages.toString()).contains("Using UNIQUE index for query for search: Patient?birthdate=2011-01-01&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale");
|
||||
assertThat(myMessages.toString()).contains("Using UNIQUE index(es) for query for search: Patient?birthdate=2011-01-01&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale");
|
||||
myMessages.clear();
|
||||
|
||||
// Other order
|
||||
|
@ -1188,7 +1276,7 @@ public class FhirResourceDaoR4ComboUniqueParamIT extends BaseComboParamsR4Test {
|
|||
results = myPatientDao.search(params, mySrd);
|
||||
assertThat(toUnqualifiedVersionlessIdValues(results)).isEmpty();
|
||||
logCapturedMessages();
|
||||
assertThat(myMessages.toString()).contains("Using UNIQUE index for query for search: Patient?birthdate=2011-01-03&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale");
|
||||
assertThat(myMessages.toString()).contains("Using UNIQUE index(es) for query for search: Patient?birthdate=2011-01-03&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale");
|
||||
myMessages.clear();
|
||||
|
||||
myMessages.clear();
|
||||
|
@ -1270,7 +1358,7 @@ public class FhirResourceDaoR4ComboUniqueParamIT extends BaseComboParamsR4Test {
|
|||
IIdType id1 = myPatientDao.update(pt1, "Patient?name=FAMILY1&organization:Organization=ORG", mySrd).getId().toUnqualifiedVersionless();
|
||||
|
||||
logCapturedMessages();
|
||||
assertThat(myMessages.toString()).contains("Using UNIQUE index for query for search: Patient?name=FAMILY1&organization=Organization%2FORG");
|
||||
assertThat(myMessages.toString()).contains("Using UNIQUE index(es) for query for search: Patient?name=FAMILY1&organization=Organization%2FORG");
|
||||
myMessages.clear();
|
||||
|
||||
runInTransaction(() -> {
|
||||
|
@ -1289,7 +1377,7 @@ public class FhirResourceDaoR4ComboUniqueParamIT extends BaseComboParamsR4Test {
|
|||
IIdType id2 = myPatientDao.update(pt1, "Patient?name=FAMILY1&organization:Organization=ORG", mySrd).getId().toUnqualifiedVersionless();
|
||||
|
||||
logCapturedMessages();
|
||||
assertThat(myMessages.toString()).contains("Using UNIQUE index for query for search: Patient?name=FAMILY1&organization=Organization%2FORG");
|
||||
assertThat(myMessages.toString()).contains("Using UNIQUE index(es) for query for search: Patient?name=FAMILY1&organization=Organization%2FORG");
|
||||
myMessages.clear();
|
||||
runInTransaction(() -> {
|
||||
List<ResourceIndexedComboStringUnique> uniques = myResourceIndexedComboStringUniqueDao.findAll();
|
||||
|
@ -1646,7 +1734,7 @@ public class FhirResourceDaoR4ComboUniqueParamIT extends BaseComboParamsR4Test {
|
|||
assertThat(toUnqualifiedVersionlessIdValues(results)).containsExactlyInAnyOrder(id2.getValue());
|
||||
|
||||
logCapturedMessages();
|
||||
assertThat(myMessages.toString()).contains("Using UNIQUE index for query for search: Patient?birthdate=2011-01-01&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale");
|
||||
assertThat(myMessages.toString()).contains("Using UNIQUE index(es) for query for search: Patient?birthdate=2011-01-01&gender=http%3A%2F%2Fhl7.org%2Ffhir%2Fadministrative-gender%7Cmale");
|
||||
myMessages.clear();
|
||||
|
||||
}
|
Loading…
Reference in New Issue