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:
James Agnew 2024-07-05 12:05:58 -04:00 committed by GitHub
parent 8a41da4a18
commit 5e519810ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 912 additions and 243 deletions

View File

@ -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;
}
}

View File

@ -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."

View File

@ -21,6 +21,7 @@ You may use the following command to get detailed help on the options:
Note the arguments:
* `-d [dialect]` &ndash; This indicates the database dialect to use. See the detailed help for a list of options
* `--enable-heavyweight-migrations` &ndash; 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

View File

@ -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);
}

View File

@ -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() {

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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));
}
}

View File

@ -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

View File

@ -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;
}
}
}

View File

@ -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();
}