001/*
002 * #%L
003 * HAPI FHIR JPA Server
004 * %%
005 * Copyright (C) 2014 - 2024 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.jpa.search.builder;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.context.RuntimeSearchParam;
024import ca.uhn.fhir.exception.TokenParamFormatInvalidRequestException;
025import ca.uhn.fhir.i18n.Msg;
026import ca.uhn.fhir.interceptor.model.RequestPartitionId;
027import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
028import ca.uhn.fhir.jpa.dao.BaseStorageDao;
029import ca.uhn.fhir.jpa.dao.predicate.SearchFilterParser;
030import ca.uhn.fhir.jpa.model.config.PartitionSettings;
031import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel;
032import ca.uhn.fhir.jpa.model.entity.TagTypeEnum;
033import ca.uhn.fhir.jpa.model.util.UcumServiceUtil;
034import ca.uhn.fhir.jpa.search.builder.models.MissingParameterQueryParams;
035import ca.uhn.fhir.jpa.search.builder.models.MissingQueryParameterPredicateParams;
036import ca.uhn.fhir.jpa.search.builder.models.PredicateBuilderCacheKey;
037import ca.uhn.fhir.jpa.search.builder.models.PredicateBuilderCacheLookupResult;
038import ca.uhn.fhir.jpa.search.builder.models.PredicateBuilderTypeEnum;
039import ca.uhn.fhir.jpa.search.builder.predicate.BaseJoiningPredicateBuilder;
040import ca.uhn.fhir.jpa.search.builder.predicate.BaseQuantityPredicateBuilder;
041import ca.uhn.fhir.jpa.search.builder.predicate.BaseSearchParamPredicateBuilder;
042import ca.uhn.fhir.jpa.search.builder.predicate.ComboNonUniqueSearchParameterPredicateBuilder;
043import ca.uhn.fhir.jpa.search.builder.predicate.ComboUniqueSearchParameterPredicateBuilder;
044import ca.uhn.fhir.jpa.search.builder.predicate.CoordsPredicateBuilder;
045import ca.uhn.fhir.jpa.search.builder.predicate.DatePredicateBuilder;
046import ca.uhn.fhir.jpa.search.builder.predicate.ICanMakeMissingParamPredicate;
047import ca.uhn.fhir.jpa.search.builder.predicate.NumberPredicateBuilder;
048import ca.uhn.fhir.jpa.search.builder.predicate.ParsedLocationParam;
049import ca.uhn.fhir.jpa.search.builder.predicate.ResourceIdPredicateBuilder;
050import ca.uhn.fhir.jpa.search.builder.predicate.ResourceLinkPredicateBuilder;
051import ca.uhn.fhir.jpa.search.builder.predicate.ResourceTablePredicateBuilder;
052import ca.uhn.fhir.jpa.search.builder.predicate.SearchParamPresentPredicateBuilder;
053import ca.uhn.fhir.jpa.search.builder.predicate.SourcePredicateBuilder;
054import ca.uhn.fhir.jpa.search.builder.predicate.StringPredicateBuilder;
055import ca.uhn.fhir.jpa.search.builder.predicate.TagPredicateBuilder;
056import ca.uhn.fhir.jpa.search.builder.predicate.TokenPredicateBuilder;
057import ca.uhn.fhir.jpa.search.builder.predicate.UriPredicateBuilder;
058import ca.uhn.fhir.jpa.search.builder.sql.PredicateBuilderFactory;
059import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder;
060import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
061import ca.uhn.fhir.jpa.searchparam.extractor.BaseSearchParamExtractor;
062import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil;
063import ca.uhn.fhir.jpa.searchparam.util.SourceParam;
064import ca.uhn.fhir.model.api.IQueryParameterAnd;
065import ca.uhn.fhir.model.api.IQueryParameterOr;
066import ca.uhn.fhir.model.api.IQueryParameterType;
067import ca.uhn.fhir.parser.DataFormatException;
068import ca.uhn.fhir.rest.api.Constants;
069import ca.uhn.fhir.rest.api.QualifiedParamList;
070import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
071import ca.uhn.fhir.rest.api.server.RequestDetails;
072import ca.uhn.fhir.rest.param.CompositeParam;
073import ca.uhn.fhir.rest.param.DateParam;
074import ca.uhn.fhir.rest.param.DateRangeParam;
075import ca.uhn.fhir.rest.param.HasParam;
076import ca.uhn.fhir.rest.param.NumberParam;
077import ca.uhn.fhir.rest.param.QuantityParam;
078import ca.uhn.fhir.rest.param.ReferenceParam;
079import ca.uhn.fhir.rest.param.SpecialParam;
080import ca.uhn.fhir.rest.param.StringParam;
081import ca.uhn.fhir.rest.param.TokenParam;
082import ca.uhn.fhir.rest.param.TokenParamModifier;
083import ca.uhn.fhir.rest.param.UriParam;
084import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
085import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
086import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
087import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
088import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
089import com.google.common.collect.Lists;
090import com.google.common.collect.Maps;
091import com.google.common.collect.Sets;
092import com.healthmarketscience.sqlbuilder.BinaryCondition;
093import com.healthmarketscience.sqlbuilder.ComboCondition;
094import com.healthmarketscience.sqlbuilder.Condition;
095import com.healthmarketscience.sqlbuilder.Expression;
096import com.healthmarketscience.sqlbuilder.InCondition;
097import com.healthmarketscience.sqlbuilder.SelectQuery;
098import com.healthmarketscience.sqlbuilder.SetOperationQuery;
099import com.healthmarketscience.sqlbuilder.Subquery;
100import com.healthmarketscience.sqlbuilder.UnionQuery;
101import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
102import jakarta.annotation.Nullable;
103import org.apache.commons.lang3.StringUtils;
104import org.apache.commons.lang3.tuple.Triple;
105import org.hl7.fhir.instance.model.api.IAnyResource;
106import org.slf4j.Logger;
107import org.slf4j.LoggerFactory;
108import org.springframework.util.CollectionUtils;
109
110import java.math.BigDecimal;
111import java.util.ArrayList;
112import java.util.Collection;
113import java.util.Collections;
114import java.util.EnumSet;
115import java.util.HashMap;
116import java.util.List;
117import java.util.Map;
118import java.util.Objects;
119import java.util.Optional;
120import java.util.Set;
121import java.util.function.Supplier;
122import java.util.regex.Pattern;
123import java.util.stream.Collectors;
124
125import static ca.uhn.fhir.jpa.search.builder.QueryStack.SearchForIdsParams.with;
126import static ca.uhn.fhir.jpa.util.QueryParameterUtils.fromOperation;
127import static ca.uhn.fhir.jpa.util.QueryParameterUtils.getChainedPart;
128import static ca.uhn.fhir.jpa.util.QueryParameterUtils.getParamNameWithPrefix;
129import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toAndPredicate;
130import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toEqualToOrInPredicate;
131import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toOperation;
132import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toOrPredicate;
133import static ca.uhn.fhir.rest.api.Constants.PARAM_HAS;
134import static ca.uhn.fhir.rest.api.Constants.PARAM_ID;
135import static org.apache.commons.lang3.StringUtils.isBlank;
136import static org.apache.commons.lang3.StringUtils.isNotBlank;
137import static org.apache.commons.lang3.StringUtils.split;
138
139public class QueryStack {
140
141        private static final Logger ourLog = LoggerFactory.getLogger(QueryStack.class);
142        public static final String LOCATION_POSITION = "Location.position";
143        private static final Pattern PATTERN_DOT_AND_ALL_AFTER = Pattern.compile("\\..*");
144
145        private final FhirContext myFhirContext;
146        private final SearchQueryBuilder mySqlBuilder;
147        private final SearchParameterMap mySearchParameters;
148        private final ISearchParamRegistry mySearchParamRegistry;
149        private final PartitionSettings myPartitionSettings;
150        private final JpaStorageSettings myStorageSettings;
151        private final EnumSet<PredicateBuilderTypeEnum> myReusePredicateBuilderTypes;
152        private Map<PredicateBuilderCacheKey, BaseJoiningPredicateBuilder> myJoinMap;
153        private Map<String, BaseJoiningPredicateBuilder> myParamNameToPredicateBuilderMap;
154        // used for _offset queries with sort, should be removed once the fix is applied to the async path too.
155        private boolean myUseAggregate;
156
157        /**
158         * Constructor
159         */
160        public QueryStack(
161                        SearchParameterMap theSearchParameters,
162                        JpaStorageSettings theStorageSettings,
163                        FhirContext theFhirContext,
164                        SearchQueryBuilder theSqlBuilder,
165                        ISearchParamRegistry theSearchParamRegistry,
166                        PartitionSettings thePartitionSettings) {
167                this(
168                                theSearchParameters,
169                                theStorageSettings,
170                                theFhirContext,
171                                theSqlBuilder,
172                                theSearchParamRegistry,
173                                thePartitionSettings,
174                                EnumSet.of(PredicateBuilderTypeEnum.DATE));
175        }
176
177        /**
178         * Constructor
179         */
180        private QueryStack(
181                        SearchParameterMap theSearchParameters,
182                        JpaStorageSettings theStorageSettings,
183                        FhirContext theFhirContext,
184                        SearchQueryBuilder theSqlBuilder,
185                        ISearchParamRegistry theSearchParamRegistry,
186                        PartitionSettings thePartitionSettings,
187                        EnumSet<PredicateBuilderTypeEnum> theReusePredicateBuilderTypes) {
188                myPartitionSettings = thePartitionSettings;
189                assert theSearchParameters != null;
190                assert theStorageSettings != null;
191                assert theFhirContext != null;
192                assert theSqlBuilder != null;
193
194                mySearchParameters = theSearchParameters;
195                myStorageSettings = theStorageSettings;
196                myFhirContext = theFhirContext;
197                mySqlBuilder = theSqlBuilder;
198                mySearchParamRegistry = theSearchParamRegistry;
199                myReusePredicateBuilderTypes = theReusePredicateBuilderTypes;
200        }
201
202        public void addSortOnCoordsNear(String theParamName, boolean theAscending, SearchParameterMap theParams) {
203                boolean handled = false;
204                if (myParamNameToPredicateBuilderMap != null) {
205                        BaseJoiningPredicateBuilder builder = myParamNameToPredicateBuilderMap.get(theParamName);
206                        if (builder instanceof CoordsPredicateBuilder) {
207                                CoordsPredicateBuilder coordsBuilder = (CoordsPredicateBuilder) builder;
208
209                                List<List<IQueryParameterType>> params = theParams.get(theParamName);
210                                if (params.size() > 0 && params.get(0).size() > 0) {
211                                        IQueryParameterType param = params.get(0).get(0);
212                                        ParsedLocationParam location = ParsedLocationParam.from(theParams, param);
213                                        double latitudeValue = location.getLatitudeValue();
214                                        double longitudeValue = location.getLongitudeValue();
215                                        mySqlBuilder.addSortCoordsNear(coordsBuilder, latitudeValue, longitudeValue, theAscending);
216                                        handled = true;
217                                }
218                        }
219                }
220
221                if (!handled) {
222                        String msg = myFhirContext
223                                        .getLocalizer()
224                                        .getMessageSanitized(QueryStack.class, "cantSortOnCoordParamWithoutValues", theParamName);
225                        throw new InvalidRequestException(Msg.code(2307) + msg);
226                }
227        }
228
229        public void addSortOnDate(String theResourceName, String theParamName, boolean theAscending) {
230                BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
231                DatePredicateBuilder datePredicateBuilder = mySqlBuilder.createDatePredicateBuilder();
232
233                Condition hashIdentityPredicate =
234                                datePredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName);
235
236                addSortCustomJoin(firstPredicateBuilder, datePredicateBuilder, hashIdentityPredicate);
237
238                mySqlBuilder.addSortDate(datePredicateBuilder.getColumnValueLow(), theAscending, myUseAggregate);
239        }
240
241        public void addSortOnLastUpdated(boolean theAscending) {
242                ResourceTablePredicateBuilder resourceTablePredicateBuilder;
243                BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
244                if (firstPredicateBuilder instanceof ResourceTablePredicateBuilder) {
245                        resourceTablePredicateBuilder = (ResourceTablePredicateBuilder) firstPredicateBuilder;
246                } else {
247                        resourceTablePredicateBuilder =
248                                        mySqlBuilder.addResourceTablePredicateBuilder(firstPredicateBuilder.getResourceIdColumn());
249                }
250                mySqlBuilder.addSortDate(resourceTablePredicateBuilder.getColumnLastUpdated(), theAscending, myUseAggregate);
251        }
252
253        public void addSortOnNumber(String theResourceName, String theParamName, boolean theAscending) {
254                BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
255                NumberPredicateBuilder numberPredicateBuilder = mySqlBuilder.createNumberPredicateBuilder();
256
257                Condition hashIdentityPredicate =
258                                numberPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName);
259
260                addSortCustomJoin(firstPredicateBuilder, numberPredicateBuilder, hashIdentityPredicate);
261
262                mySqlBuilder.addSortNumeric(numberPredicateBuilder.getColumnValue(), theAscending, myUseAggregate);
263        }
264
265        public void addSortOnQuantity(String theResourceName, String theParamName, boolean theAscending) {
266                BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
267
268                BaseQuantityPredicateBuilder quantityPredicateBuilder = mySqlBuilder.createQuantityPredicateBuilder();
269
270                Condition hashIdentityPredicate =
271                                quantityPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName);
272
273                addSortCustomJoin(firstPredicateBuilder, quantityPredicateBuilder, hashIdentityPredicate);
274
275                mySqlBuilder.addSortNumeric(quantityPredicateBuilder.getColumnValue(), theAscending, myUseAggregate);
276        }
277
278        public void addSortOnResourceId(boolean theAscending) {
279                ResourceTablePredicateBuilder resourceTablePredicateBuilder;
280                BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
281                if (firstPredicateBuilder instanceof ResourceTablePredicateBuilder) {
282                        resourceTablePredicateBuilder = (ResourceTablePredicateBuilder) firstPredicateBuilder;
283                } else {
284                        resourceTablePredicateBuilder =
285                                        mySqlBuilder.addResourceTablePredicateBuilder(firstPredicateBuilder.getResourceIdColumn());
286                }
287                mySqlBuilder.addSortString(resourceTablePredicateBuilder.getColumnFhirId(), theAscending, myUseAggregate);
288        }
289
290        /** Sort on RES_ID -- used to break ties for reliable sort */
291        public void addSortOnResourcePID(boolean theAscending) {
292                BaseJoiningPredicateBuilder predicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
293                mySqlBuilder.addSortString(predicateBuilder.getResourceIdColumn(), theAscending);
294        }
295
296        public void addSortOnResourceLink(
297                        String theResourceName,
298                        String theReferenceTargetType,
299                        String theParamName,
300                        String theChain,
301                        boolean theAscending,
302                        SearchParameterMap theParams) {
303                BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
304                ResourceLinkPredicateBuilder resourceLinkPredicateBuilder = mySqlBuilder.createReferencePredicateBuilder(this);
305
306                Condition pathPredicate =
307                                resourceLinkPredicateBuilder.createPredicateSourcePaths(theResourceName, theParamName);
308
309                addSortCustomJoin(firstPredicateBuilder, resourceLinkPredicateBuilder, pathPredicate);
310
311                if (isBlank(theChain)) {
312                        mySqlBuilder.addSortNumeric(
313                                        resourceLinkPredicateBuilder.getColumnTargetResourceId(), theAscending, myUseAggregate);
314                        return;
315                }
316
317                String targetType = null;
318                RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName);
319                if (theReferenceTargetType != null) {
320                        targetType = theReferenceTargetType;
321                } else if (param.getTargets().size() > 1) {
322                        throw new InvalidRequestException(Msg.code(2287) + "Unable to sort on a chained parameter from '"
323                                        + theParamName + "' as this parameter has multiple target types. Please specify the target type.");
324                } else if (param.getTargets().size() == 1) {
325                        targetType = param.getTargets().iterator().next();
326                }
327
328                if (isBlank(targetType)) {
329                        throw new InvalidRequestException(
330                                        Msg.code(2288) + "Unable to sort on a chained parameter from '" + theParamName
331                                                        + "' as this parameter as this parameter does not define a target type. Please specify the target type.");
332                }
333
334                RuntimeSearchParam targetSearchParameter = mySearchParamRegistry.getActiveSearchParam(targetType, theChain);
335                if (targetSearchParameter == null) {
336                        Collection<String> validSearchParameterNames =
337                                        mySearchParamRegistry.getActiveSearchParams(targetType).values().stream()
338                                                        .filter(t -> t.getParamType() == RestSearchParameterTypeEnum.STRING
339                                                                        || t.getParamType() == RestSearchParameterTypeEnum.TOKEN
340                                                                        || t.getParamType() == RestSearchParameterTypeEnum.DATE)
341                                                        .map(RuntimeSearchParam::getName)
342                                                        .sorted()
343                                                        .distinct()
344                                                        .collect(Collectors.toList());
345                        String msg = myFhirContext
346                                        .getLocalizer()
347                                        .getMessageSanitized(
348                                                        BaseStorageDao.class,
349                                                        "invalidSortParameter",
350                                                        theChain,
351                                                        targetType,
352                                                        validSearchParameterNames);
353                        throw new InvalidRequestException(Msg.code(2289) + msg);
354                }
355
356                BaseSearchParamPredicateBuilder chainedPredicateBuilder;
357                DbColumn[] sortColumn;
358                switch (targetSearchParameter.getParamType()) {
359                        case STRING:
360                                StringPredicateBuilder stringPredicateBuilder = mySqlBuilder.createStringPredicateBuilder();
361                                sortColumn = new DbColumn[] {stringPredicateBuilder.getColumnValueNormalized()};
362                                chainedPredicateBuilder = stringPredicateBuilder;
363                                break;
364                        case TOKEN:
365                                TokenPredicateBuilder tokenPredicateBuilder = mySqlBuilder.createTokenPredicateBuilder();
366                                sortColumn =
367                                                new DbColumn[] {tokenPredicateBuilder.getColumnSystem(), tokenPredicateBuilder.getColumnValue()
368                                                };
369                                chainedPredicateBuilder = tokenPredicateBuilder;
370                                break;
371                        case DATE:
372                                DatePredicateBuilder datePredicateBuilder = mySqlBuilder.createDatePredicateBuilder();
373                                sortColumn = new DbColumn[] {datePredicateBuilder.getColumnValueLow()};
374                                chainedPredicateBuilder = datePredicateBuilder;
375                                break;
376
377                                /*
378                                 * Note that many of the options below aren't implemented because they
379                                 * don't seem useful to me, but they could theoretically be implemented
380                                 * if someone ever needed them. I'm not sure why you'd want to do a chained
381                                 * sort on a target that was a reference or a quantity, but if someone needed
382                                 * that we could implement it here.
383                                 */
384                        case SPECIAL: {
385                                if (LOCATION_POSITION.equals(targetSearchParameter.getPath())) {
386                                        List<List<IQueryParameterType>> params = theParams.get(theParamName);
387                                        if (params != null && !params.isEmpty() && !params.get(0).isEmpty()) {
388                                                IQueryParameterType locationParam = params.get(0).get(0);
389                                                final SpecialParam specialParam =
390                                                                new SpecialParam().setValue(locationParam.getValueAsQueryToken(myFhirContext));
391                                                ParsedLocationParam location = ParsedLocationParam.from(theParams, specialParam);
392                                                double latitudeValue = location.getLatitudeValue();
393                                                double longitudeValue = location.getLongitudeValue();
394                                                final CoordsPredicateBuilder coordsPredicateBuilder = mySqlBuilder.addCoordsPredicateBuilder(
395                                                                resourceLinkPredicateBuilder.getColumnTargetResourceId());
396                                                mySqlBuilder.addSortCoordsNear(
397                                                                coordsPredicateBuilder, latitudeValue, longitudeValue, theAscending);
398                                        } else {
399                                                String msg = myFhirContext
400                                                                .getLocalizer()
401                                                                .getMessageSanitized(
402                                                                                QueryStack.class, "cantSortOnCoordParamWithoutValues", theParamName);
403                                                throw new InvalidRequestException(Msg.code(2497) + msg);
404                                        }
405                                        return;
406                                }
407                        }
408                        case NUMBER:
409                        case REFERENCE:
410                        case COMPOSITE:
411                        case QUANTITY:
412                        case URI:
413                        case HAS:
414
415                        default:
416                                throw new InvalidRequestException(Msg.code(2290) + "Unable to sort on a chained parameter "
417                                                + theParamName + "." + theChain + " as this parameter. Can not sort on chains of target type: "
418                                                + targetSearchParameter.getParamType().name());
419                }
420
421                addSortCustomJoin(resourceLinkPredicateBuilder.getColumnTargetResourceId(), chainedPredicateBuilder, null);
422                Condition predicate = chainedPredicateBuilder.createHashIdentityPredicate(targetType, theChain);
423                mySqlBuilder.addPredicate(predicate);
424
425                for (DbColumn next : sortColumn) {
426                        mySqlBuilder.addSortNumeric(next, theAscending, myUseAggregate);
427                }
428        }
429
430        public void addSortOnString(String theResourceName, String theParamName, boolean theAscending) {
431                BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
432
433                StringPredicateBuilder stringPredicateBuilder = mySqlBuilder.createStringPredicateBuilder();
434                Condition hashIdentityPredicate =
435                                stringPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName);
436
437                addSortCustomJoin(firstPredicateBuilder, stringPredicateBuilder, hashIdentityPredicate);
438
439                mySqlBuilder.addSortString(stringPredicateBuilder.getColumnValueNormalized(), theAscending, myUseAggregate);
440        }
441
442        public void addSortOnToken(String theResourceName, String theParamName, boolean theAscending) {
443                BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
444
445                TokenPredicateBuilder tokenPredicateBuilder = mySqlBuilder.createTokenPredicateBuilder();
446                Condition hashIdentityPredicate =
447                                tokenPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName);
448
449                addSortCustomJoin(firstPredicateBuilder, tokenPredicateBuilder, hashIdentityPredicate);
450
451                mySqlBuilder.addSortString(tokenPredicateBuilder.getColumnSystem(), theAscending, myUseAggregate);
452                mySqlBuilder.addSortString(tokenPredicateBuilder.getColumnValue(), theAscending, myUseAggregate);
453        }
454
455        public void addSortOnUri(String theResourceName, String theParamName, boolean theAscending) {
456                BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
457
458                UriPredicateBuilder uriPredicateBuilder = mySqlBuilder.createUriPredicateBuilder();
459                Condition hashIdentityPredicate =
460                                uriPredicateBuilder.createHashIdentityPredicate(theResourceName, theParamName);
461
462                addSortCustomJoin(firstPredicateBuilder, uriPredicateBuilder, hashIdentityPredicate);
463
464                mySqlBuilder.addSortString(uriPredicateBuilder.getColumnValue(), theAscending, myUseAggregate);
465        }
466
467        private void addSortCustomJoin(
468                        BaseJoiningPredicateBuilder theFromJoiningPredicateBuilder,
469                        BaseJoiningPredicateBuilder theToJoiningPredicateBuilder,
470                        Condition theCondition) {
471                addSortCustomJoin(
472                                theFromJoiningPredicateBuilder.getResourceIdColumn(), theToJoiningPredicateBuilder, theCondition);
473        }
474
475        private void addSortCustomJoin(
476                        DbColumn theFromDbColumn,
477                        BaseJoiningPredicateBuilder theToJoiningPredicateBuilder,
478                        Condition theCondition) {
479                ComboCondition onCondition =
480                                mySqlBuilder.createOnCondition(theFromDbColumn, theToJoiningPredicateBuilder.getResourceIdColumn());
481
482                if (theCondition != null) {
483                        onCondition.addCondition(theCondition);
484                }
485
486                mySqlBuilder.addCustomJoin(
487                                SelectQuery.JoinType.LEFT_OUTER,
488                                theFromDbColumn.getTable(),
489                                theToJoiningPredicateBuilder.getTable(),
490                                onCondition);
491        }
492
493        public void setUseAggregate(boolean theUseAggregate) {
494                myUseAggregate = theUseAggregate;
495        }
496
497        @SuppressWarnings("unchecked")
498        private <T extends BaseJoiningPredicateBuilder> PredicateBuilderCacheLookupResult<T> createOrReusePredicateBuilder(
499                        PredicateBuilderTypeEnum theType,
500                        DbColumn theSourceJoinColumn,
501                        String theParamName,
502                        Supplier<T> theFactoryMethod) {
503                boolean cacheHit = false;
504                BaseJoiningPredicateBuilder retVal;
505                if (myReusePredicateBuilderTypes.contains(theType)) {
506                        PredicateBuilderCacheKey key = new PredicateBuilderCacheKey(theSourceJoinColumn, theType, theParamName);
507                        if (myJoinMap == null) {
508                                myJoinMap = new HashMap<>();
509                        }
510                        retVal = myJoinMap.get(key);
511                        if (retVal != null) {
512                                cacheHit = true;
513                        } else {
514                                retVal = theFactoryMethod.get();
515                                myJoinMap.put(key, retVal);
516                        }
517                } else {
518                        retVal = theFactoryMethod.get();
519                }
520
521                if (theType == PredicateBuilderTypeEnum.COORDS) {
522                        if (myParamNameToPredicateBuilderMap == null) {
523                                myParamNameToPredicateBuilderMap = new HashMap<>();
524                        }
525                        myParamNameToPredicateBuilderMap.put(theParamName, retVal);
526                }
527
528                return new PredicateBuilderCacheLookupResult<>(cacheHit, (T) retVal);
529        }
530
531        private Condition createPredicateComposite(
532                        @Nullable DbColumn theSourceJoinColumn,
533                        String theResourceName,
534                        String theSpnamePrefix,
535                        RuntimeSearchParam theParamDef,
536                        List<? extends IQueryParameterType> theNextAnd,
537                        RequestPartitionId theRequestPartitionId) {
538                return createPredicateComposite(
539                                theSourceJoinColumn,
540                                theResourceName,
541                                theSpnamePrefix,
542                                theParamDef,
543                                theNextAnd,
544                                theRequestPartitionId,
545                                mySqlBuilder);
546        }
547
548        private Condition createPredicateComposite(
549                        @Nullable DbColumn theSourceJoinColumn,
550                        String theResourceName,
551                        String theSpnamePrefix,
552                        RuntimeSearchParam theParamDef,
553                        List<? extends IQueryParameterType> theNextAnd,
554                        RequestPartitionId theRequestPartitionId,
555                        SearchQueryBuilder theSqlBuilder) {
556
557                Condition orCondidtion = null;
558                for (IQueryParameterType next : theNextAnd) {
559
560                        if (!(next instanceof CompositeParam<?, ?>)) {
561                                throw new InvalidRequestException(Msg.code(1203) + "Invalid type for composite param (must be "
562                                                + CompositeParam.class.getSimpleName() + ": " + next.getClass());
563                        }
564                        CompositeParam<?, ?> cp = (CompositeParam<?, ?>) next;
565
566                        List<RuntimeSearchParam> componentParams =
567                                        JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, theParamDef);
568                        RuntimeSearchParam left = componentParams.get(0);
569                        IQueryParameterType leftValue = cp.getLeftValue();
570                        Condition leftPredicate = createPredicateCompositePart(
571                                        theSourceJoinColumn,
572                                        theResourceName,
573                                        theSpnamePrefix,
574                                        left,
575                                        leftValue,
576                                        theRequestPartitionId,
577                                        theSqlBuilder);
578
579                        RuntimeSearchParam right = componentParams.get(1);
580                        IQueryParameterType rightValue = cp.getRightValue();
581                        Condition rightPredicate = createPredicateCompositePart(
582                                        theSourceJoinColumn,
583                                        theResourceName,
584                                        theSpnamePrefix,
585                                        right,
586                                        rightValue,
587                                        theRequestPartitionId,
588                                        theSqlBuilder);
589
590                        Condition andCondition = toAndPredicate(leftPredicate, rightPredicate);
591
592                        if (orCondidtion == null) {
593                                orCondidtion = toOrPredicate(andCondition);
594                        } else {
595                                orCondidtion = toOrPredicate(orCondidtion, andCondition);
596                        }
597                }
598
599                return orCondidtion;
600        }
601
602        private Condition createPredicateCompositePart(
603                        @Nullable DbColumn theSourceJoinColumn,
604                        String theResourceName,
605                        String theSpnamePrefix,
606                        RuntimeSearchParam theParam,
607                        IQueryParameterType theParamValue,
608                        RequestPartitionId theRequestPartitionId,
609                        SearchQueryBuilder theSqlBuilder) {
610
611                switch (theParam.getParamType()) {
612                        case STRING: {
613                                return createPredicateString(
614                                                theSourceJoinColumn,
615                                                theResourceName,
616                                                theSpnamePrefix,
617                                                theParam,
618                                                Collections.singletonList(theParamValue),
619                                                null,
620                                                theRequestPartitionId,
621                                                theSqlBuilder);
622                        }
623                        case TOKEN: {
624                                return createPredicateToken(
625                                                theSourceJoinColumn,
626                                                theResourceName,
627                                                theSpnamePrefix,
628                                                theParam,
629                                                Collections.singletonList(theParamValue),
630                                                null,
631                                                theRequestPartitionId,
632                                                theSqlBuilder);
633                        }
634                        case DATE: {
635                                return createPredicateDate(
636                                                theSourceJoinColumn,
637                                                theResourceName,
638                                                theSpnamePrefix,
639                                                theParam,
640                                                Collections.singletonList(theParamValue),
641                                                toOperation(((DateParam) theParamValue).getPrefix()),
642                                                theRequestPartitionId,
643                                                theSqlBuilder);
644                        }
645                        case QUANTITY: {
646                                return createPredicateQuantity(
647                                                theSourceJoinColumn,
648                                                theResourceName,
649                                                theSpnamePrefix,
650                                                theParam,
651                                                Collections.singletonList(theParamValue),
652                                                null,
653                                                theRequestPartitionId,
654                                                theSqlBuilder);
655                        }
656                        case NUMBER:
657                        case REFERENCE:
658                        case COMPOSITE:
659                        case URI:
660                        case HAS:
661                        case SPECIAL:
662                        default:
663                                throw new InvalidRequestException(Msg.code(1204)
664                                                + "Don't know how to handle composite parameter with type of " + theParam.getParamType());
665                }
666        }
667
668        private Condition createMissingParameterQuery(MissingParameterQueryParams theParams) {
669                if (theParams.getParamType() == RestSearchParameterTypeEnum.COMPOSITE) {
670                        ourLog.error("Cannot create missing parameter query for a composite parameter.");
671                        return null;
672                } else if (theParams.getParamType() == RestSearchParameterTypeEnum.REFERENCE) {
673                        if (isEligibleForEmbeddedChainedResourceSearch(
674                                                        theParams.getResourceType(), theParams.getParamName(), theParams.getQueryParameterTypes())
675                                        .supportsUplifted()) {
676                                ourLog.error("Cannot construct missing query parameter search for ContainedResource REFERENCE search.");
677                                return null;
678                        }
679                }
680
681                // TODO - Change this when we have HFJ_SPIDX_MISSING table
682                /**
683                 * How we search depends on if the
684                 * {@link JpaStorageSettings#getIndexMissingFields()} property
685                 * is Enabled or Disabled.
686                 *
687                 * If it is, we will use the SP_MISSING values set into the various
688                 * SP_INDX_X tables and search on those ("old" search).
689                 *
690                 * If it is not set, however, we will try and construct a query that
691                 * looks for missing SearchParameters in the SP_IDX_* tables ("new" search).
692                 *
693                 * You cannot mix and match, however (SP_MISSING is not in HASH_IDENTITY information).
694                 * So setting (or unsetting) the IndexMissingFields
695                 * property should always be followed up with a /$reindex call.
696                 *
697                 * ---
698                 *
699                 * Current limitations:
700                 * Checking if a row exists ("new" search) for a given missing field in an SP_INDX_* table
701                 * (ie, :missing=true) is slow when there are many resources in the table. (Defaults to
702                 * a table scan, since HASH_IDENTITY isn't part of the index).
703                 *
704                 * However, the "old" search method was slow for the reverse: when looking for resources
705                 * that do not have a missing field (:missing=false) for much the same reason.
706                 */
707                SearchQueryBuilder sqlBuilder = theParams.getSqlBuilder();
708                if (myStorageSettings.getIndexMissingFields() == JpaStorageSettings.IndexEnabledEnum.DISABLED) {
709                        // new search
710                        return createMissingPredicateForUnindexedMissingFields(theParams, sqlBuilder);
711                } else {
712                        // old search
713                        return createMissingPredicateForIndexedMissingFields(theParams, sqlBuilder);
714                }
715        }
716
717        /**
718         * Old way of searching.
719         * Missing values must be indexed!
720         */
721        private Condition createMissingPredicateForIndexedMissingFields(
722                        MissingParameterQueryParams theParams, SearchQueryBuilder sqlBuilder) {
723                PredicateBuilderTypeEnum predicateType = null;
724                Supplier<? extends BaseJoiningPredicateBuilder> supplier = null;
725                switch (theParams.getParamType()) {
726                        case STRING:
727                                predicateType = PredicateBuilderTypeEnum.STRING;
728                                supplier = () -> sqlBuilder.addStringPredicateBuilder(theParams.getSourceJoinColumn());
729                                break;
730                        case NUMBER:
731                                predicateType = PredicateBuilderTypeEnum.NUMBER;
732                                supplier = () -> sqlBuilder.addNumberPredicateBuilder(theParams.getSourceJoinColumn());
733                                break;
734                        case DATE:
735                                predicateType = PredicateBuilderTypeEnum.DATE;
736                                supplier = () -> sqlBuilder.addDatePredicateBuilder(theParams.getSourceJoinColumn());
737                                break;
738                        case TOKEN:
739                                predicateType = PredicateBuilderTypeEnum.TOKEN;
740                                supplier = () -> sqlBuilder.addTokenPredicateBuilder(theParams.getSourceJoinColumn());
741                                break;
742                        case QUANTITY:
743                                predicateType = PredicateBuilderTypeEnum.QUANTITY;
744                                supplier = () -> sqlBuilder.addQuantityPredicateBuilder(theParams.getSourceJoinColumn());
745                                break;
746                        case REFERENCE:
747                        case URI:
748                                // we expect these values, but the pattern is slightly different;
749                                // see below
750                                break;
751                        case HAS:
752                        case SPECIAL:
753                                predicateType = PredicateBuilderTypeEnum.COORDS;
754                                supplier = () -> sqlBuilder.addCoordsPredicateBuilder(theParams.getSourceJoinColumn());
755                                break;
756                        case COMPOSITE:
757                        default:
758                                break;
759                }
760
761                if (supplier != null) {
762                        BaseSearchParamPredicateBuilder join = (BaseSearchParamPredicateBuilder) createOrReusePredicateBuilder(
763                                                        predicateType, theParams.getSourceJoinColumn(), theParams.getParamName(), supplier)
764                                        .getResult();
765
766                        return join.createPredicateParamMissingForNonReference(
767                                        theParams.getResourceType(),
768                                        theParams.getParamName(),
769                                        theParams.isMissing(),
770                                        theParams.getRequestPartitionId());
771                } else {
772                        if (theParams.getParamType() == RestSearchParameterTypeEnum.REFERENCE) {
773                                SearchParamPresentPredicateBuilder join =
774                                                sqlBuilder.addSearchParamPresentPredicateBuilder(theParams.getSourceJoinColumn());
775                                return join.createPredicateParamMissingForReference(
776                                                theParams.getResourceType(),
777                                                theParams.getParamName(),
778                                                theParams.isMissing(),
779                                                theParams.getRequestPartitionId());
780                        } else if (theParams.getParamType() == RestSearchParameterTypeEnum.URI) {
781                                UriPredicateBuilder join = sqlBuilder.addUriPredicateBuilder(theParams.getSourceJoinColumn());
782                                return join.createPredicateParamMissingForNonReference(
783                                                theParams.getResourceType(),
784                                                theParams.getParamName(),
785                                                theParams.isMissing(),
786                                                theParams.getRequestPartitionId());
787                        } else {
788                                // we don't expect to see this
789                                ourLog.error("Invalid param type " + theParams.getParamType().name());
790                                return null;
791                        }
792                }
793        }
794
795        /**
796         * New way of searching for missing fields.
797         * Missing values must not indexed!
798         */
799        private Condition createMissingPredicateForUnindexedMissingFields(
800                        MissingParameterQueryParams theParams, SearchQueryBuilder sqlBuilder) {
801                ResourceTablePredicateBuilder table = sqlBuilder.getOrCreateResourceTablePredicateBuilder();
802
803                ICanMakeMissingParamPredicate innerQuery = PredicateBuilderFactory.createPredicateBuilderForParamType(
804                                theParams.getParamType(), theParams.getSqlBuilder(), this);
805
806                return innerQuery.createPredicateParamMissingValue(new MissingQueryParameterPredicateParams(
807                                table, theParams.isMissing(), theParams.getParamName(), theParams.getRequestPartitionId()));
808        }
809
810        public Condition createPredicateCoords(
811                        @Nullable DbColumn theSourceJoinColumn,
812                        String theResourceName,
813                        String theSpnamePrefix,
814                        RuntimeSearchParam theSearchParam,
815                        List<? extends IQueryParameterType> theList,
816                        RequestPartitionId theRequestPartitionId,
817                        SearchQueryBuilder theSqlBuilder) {
818                Boolean isMissing = theList.get(0).getMissing();
819                if (isMissing != null) {
820                        String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
821
822                        return createMissingParameterQuery(new MissingParameterQueryParams(
823                                        theSqlBuilder,
824                                        theSearchParam.getParamType(),
825                                        theList,
826                                        paramName,
827                                        theResourceName,
828                                        theSourceJoinColumn,
829                                        theRequestPartitionId));
830                } else {
831                        CoordsPredicateBuilder predicateBuilder = createOrReusePredicateBuilder(
832                                                        PredicateBuilderTypeEnum.COORDS,
833                                                        theSourceJoinColumn,
834                                                        theSearchParam.getName(),
835                                                        () -> mySqlBuilder.addCoordsPredicateBuilder(theSourceJoinColumn))
836                                        .getResult();
837
838                        List<Condition> codePredicates = new ArrayList<>();
839                        for (IQueryParameterType nextOr : theList) {
840                                Condition singleCode = predicateBuilder.createPredicateCoords(
841                                                mySearchParameters,
842                                                nextOr,
843                                                theResourceName,
844                                                theSearchParam,
845                                                predicateBuilder,
846                                                theRequestPartitionId);
847                                codePredicates.add(singleCode);
848                        }
849
850                        return predicateBuilder.combineWithRequestPartitionIdPredicate(
851                                        theRequestPartitionId, ComboCondition.or(codePredicates.toArray(new Condition[0])));
852                }
853        }
854
855        public Condition createPredicateDate(
856                        @Nullable DbColumn theSourceJoinColumn,
857                        String theResourceName,
858                        String theSpnamePrefix,
859                        RuntimeSearchParam theSearchParam,
860                        List<? extends IQueryParameterType> theList,
861                        SearchFilterParser.CompareOperation theOperation,
862                        RequestPartitionId theRequestPartitionId) {
863                return createPredicateDate(
864                                theSourceJoinColumn,
865                                theResourceName,
866                                theSpnamePrefix,
867                                theSearchParam,
868                                theList,
869                                theOperation,
870                                theRequestPartitionId,
871                                mySqlBuilder);
872        }
873
874        public Condition createPredicateDate(
875                        @Nullable DbColumn theSourceJoinColumn,
876                        String theResourceName,
877                        String theSpnamePrefix,
878                        RuntimeSearchParam theSearchParam,
879                        List<? extends IQueryParameterType> theList,
880                        SearchFilterParser.CompareOperation theOperation,
881                        RequestPartitionId theRequestPartitionId,
882                        SearchQueryBuilder theSqlBuilder) {
883                String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
884
885                Boolean isMissing = theList.get(0).getMissing();
886                if (isMissing != null) {
887                        return createMissingParameterQuery(new MissingParameterQueryParams(
888                                        theSqlBuilder,
889                                        theSearchParam.getParamType(),
890                                        theList,
891                                        paramName,
892                                        theResourceName,
893                                        theSourceJoinColumn,
894                                        theRequestPartitionId));
895                } else {
896                        PredicateBuilderCacheLookupResult<DatePredicateBuilder> predicateBuilderLookupResult =
897                                        createOrReusePredicateBuilder(
898                                                        PredicateBuilderTypeEnum.DATE,
899                                                        theSourceJoinColumn,
900                                                        paramName,
901                                                        () -> theSqlBuilder.addDatePredicateBuilder(theSourceJoinColumn));
902                        DatePredicateBuilder predicateBuilder = predicateBuilderLookupResult.getResult();
903                        boolean cacheHit = predicateBuilderLookupResult.isCacheHit();
904
905                        List<Condition> codePredicates = new ArrayList<>();
906
907                        for (IQueryParameterType nextOr : theList) {
908                                Condition p = predicateBuilder.createPredicateDateWithoutIdentityPredicate(nextOr, theOperation);
909                                codePredicates.add(p);
910                        }
911
912                        Condition predicate = toOrPredicate(codePredicates);
913
914                        if (!cacheHit) {
915                                predicate = predicateBuilder.combineWithHashIdentityPredicate(theResourceName, paramName, predicate);
916                                predicate = predicateBuilder.combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate);
917                        }
918
919                        return predicate;
920                }
921        }
922
923        private Condition createPredicateFilter(
924                        QueryStack theQueryStack3,
925                        SearchFilterParser.BaseFilter theFilter,
926                        String theResourceName,
927                        RequestDetails theRequest,
928                        RequestPartitionId theRequestPartitionId) {
929
930                if (theFilter instanceof SearchFilterParser.FilterParameter) {
931                        return createPredicateFilter(
932                                        theQueryStack3,
933                                        (SearchFilterParser.FilterParameter) theFilter,
934                                        theResourceName,
935                                        theRequest,
936                                        theRequestPartitionId);
937                } else if (theFilter instanceof SearchFilterParser.FilterLogical) {
938                        // Left side
939                        Condition xPredicate = createPredicateFilter(
940                                        theQueryStack3,
941                                        ((SearchFilterParser.FilterLogical) theFilter).getFilter1(),
942                                        theResourceName,
943                                        theRequest,
944                                        theRequestPartitionId);
945
946                        // Right side
947                        Condition yPredicate = createPredicateFilter(
948                                        theQueryStack3,
949                                        ((SearchFilterParser.FilterLogical) theFilter).getFilter2(),
950                                        theResourceName,
951                                        theRequest,
952                                        theRequestPartitionId);
953
954                        if (((SearchFilterParser.FilterLogical) theFilter).getOperation()
955                                        == SearchFilterParser.FilterLogicalOperation.and) {
956                                return ComboCondition.and(xPredicate, yPredicate);
957                        } else if (((SearchFilterParser.FilterLogical) theFilter).getOperation()
958                                        == SearchFilterParser.FilterLogicalOperation.or) {
959                                return ComboCondition.or(xPredicate, yPredicate);
960                        } else {
961                                // Shouldn't happen
962                                throw new InvalidRequestException(Msg.code(1205) + "Don't know how to handle operation "
963                                                + ((SearchFilterParser.FilterLogical) theFilter).getOperation());
964                        }
965                } else {
966                        return createPredicateFilter(
967                                        theQueryStack3,
968                                        ((SearchFilterParser.FilterParameterGroup) theFilter).getContained(),
969                                        theResourceName,
970                                        theRequest,
971                                        theRequestPartitionId);
972                }
973        }
974
975        private Condition createPredicateFilter(
976                        QueryStack theQueryStack3,
977                        SearchFilterParser.FilterParameter theFilter,
978                        String theResourceName,
979                        RequestDetails theRequest,
980                        RequestPartitionId theRequestPartitionId) {
981
982                String paramName = theFilter.getParamPath().getName();
983
984                switch (paramName) {
985                        case IAnyResource.SP_RES_ID: {
986                                TokenParam param = new TokenParam();
987                                param.setValueAsQueryToken(null, null, null, theFilter.getValue());
988                                return theQueryStack3.createPredicateResourceId(
989                                                null,
990                                                Collections.singletonList(Collections.singletonList(param)),
991                                                theResourceName,
992                                                theFilter.getOperation(),
993                                                theRequestPartitionId);
994                        }
995                        case Constants.PARAM_SOURCE: {
996                                TokenParam param = new TokenParam();
997                                param.setValueAsQueryToken(null, null, null, theFilter.getValue());
998                                return createPredicateSource(null, Collections.singletonList(param));
999                        }
1000                        default:
1001                                RuntimeSearchParam searchParam = mySearchParamRegistry.getActiveSearchParam(theResourceName, paramName);
1002                                if (searchParam == null) {
1003                                        Collection<String> validNames =
1004                                                        mySearchParamRegistry.getValidSearchParameterNamesIncludingMeta(theResourceName);
1005                                        String msg = myFhirContext
1006                                                        .getLocalizer()
1007                                                        .getMessageSanitized(
1008                                                                        BaseStorageDao.class,
1009                                                                        "invalidSearchParameter",
1010                                                                        paramName,
1011                                                                        theResourceName,
1012                                                                        validNames);
1013                                        throw new InvalidRequestException(Msg.code(1206) + msg);
1014                                }
1015                                RestSearchParameterTypeEnum typeEnum = searchParam.getParamType();
1016                                if (typeEnum == RestSearchParameterTypeEnum.URI) {
1017                                        return theQueryStack3.createPredicateUri(
1018                                                        null,
1019                                                        theResourceName,
1020                                                        null,
1021                                                        searchParam,
1022                                                        Collections.singletonList(new UriParam(theFilter.getValue())),
1023                                                        theFilter.getOperation(),
1024                                                        theRequest,
1025                                                        theRequestPartitionId);
1026                                } else if (typeEnum == RestSearchParameterTypeEnum.STRING) {
1027                                        return theQueryStack3.createPredicateString(
1028                                                        null,
1029                                                        theResourceName,
1030                                                        null,
1031                                                        searchParam,
1032                                                        Collections.singletonList(new StringParam(theFilter.getValue())),
1033                                                        theFilter.getOperation(),
1034                                                        theRequestPartitionId);
1035                                } else if (typeEnum == RestSearchParameterTypeEnum.DATE) {
1036                                        return theQueryStack3.createPredicateDate(
1037                                                        null,
1038                                                        theResourceName,
1039                                                        null,
1040                                                        searchParam,
1041                                                        Collections.singletonList(
1042                                                                        new DateParam(fromOperation(theFilter.getOperation()), theFilter.getValue())),
1043                                                        theFilter.getOperation(),
1044                                                        theRequestPartitionId);
1045                                } else if (typeEnum == RestSearchParameterTypeEnum.NUMBER) {
1046                                        return theQueryStack3.createPredicateNumber(
1047                                                        null,
1048                                                        theResourceName,
1049                                                        null,
1050                                                        searchParam,
1051                                                        Collections.singletonList(new NumberParam(theFilter.getValue())),
1052                                                        theFilter.getOperation(),
1053                                                        theRequestPartitionId);
1054                                } else if (typeEnum == RestSearchParameterTypeEnum.REFERENCE) {
1055                                        SearchFilterParser.CompareOperation operation = theFilter.getOperation();
1056                                        String resourceType =
1057                                                        null; // The value can either have (Patient/123) or not have (123) a resource type, either
1058                                        // way it's not needed here
1059                                        String chain = (theFilter.getParamPath().getNext() != null)
1060                                                        ? theFilter.getParamPath().getNext().toString()
1061                                                        : null;
1062                                        String value = theFilter.getValue();
1063                                        ReferenceParam referenceParam = new ReferenceParam(resourceType, chain, value);
1064                                        return theQueryStack3.createPredicateReference(
1065                                                        null,
1066                                                        theResourceName,
1067                                                        paramName,
1068                                                        new ArrayList<>(),
1069                                                        Collections.singletonList(referenceParam),
1070                                                        operation,
1071                                                        theRequest,
1072                                                        theRequestPartitionId);
1073                                } else if (typeEnum == RestSearchParameterTypeEnum.QUANTITY) {
1074                                        return theQueryStack3.createPredicateQuantity(
1075                                                        null,
1076                                                        theResourceName,
1077                                                        null,
1078                                                        searchParam,
1079                                                        Collections.singletonList(new QuantityParam(theFilter.getValue())),
1080                                                        theFilter.getOperation(),
1081                                                        theRequestPartitionId);
1082                                } else if (typeEnum == RestSearchParameterTypeEnum.COMPOSITE) {
1083                                        throw new InvalidRequestException(Msg.code(1207)
1084                                                        + "Composite search parameters not currently supported with _filter clauses");
1085                                } else if (typeEnum == RestSearchParameterTypeEnum.TOKEN) {
1086                                        TokenParam param = new TokenParam();
1087                                        param.setValueAsQueryToken(null, null, null, theFilter.getValue());
1088                                        return theQueryStack3.createPredicateToken(
1089                                                        null,
1090                                                        theResourceName,
1091                                                        null,
1092                                                        searchParam,
1093                                                        Collections.singletonList(param),
1094                                                        theFilter.getOperation(),
1095                                                        theRequestPartitionId);
1096                                }
1097                                break;
1098                }
1099                return null;
1100        }
1101
1102        private Condition createPredicateHas(
1103                        @Nullable DbColumn theSourceJoinColumn,
1104                        String theResourceType,
1105                        List<List<IQueryParameterType>> theHasParameters,
1106                        RequestDetails theRequest,
1107                        RequestPartitionId theRequestPartitionId) {
1108
1109                List<Condition> andPredicates = new ArrayList<>();
1110                for (List<? extends IQueryParameterType> nextOrList : theHasParameters) {
1111
1112                        String targetResourceType = null;
1113                        String paramReference = null;
1114                        String parameterName = null;
1115
1116                        String paramName = null;
1117                        List<QualifiedParamList> parameters = new ArrayList<>();
1118                        for (IQueryParameterType nextParam : nextOrList) {
1119                                HasParam next = (HasParam) nextParam;
1120                                targetResourceType = next.getTargetResourceType();
1121                                paramReference = next.getReferenceFieldName();
1122                                parameterName = next.getParameterName();
1123                                paramName = PATTERN_DOT_AND_ALL_AFTER.matcher(parameterName).replaceAll("");
1124                                parameters.add(QualifiedParamList.singleton(null, next.getValueAsQueryToken(myFhirContext)));
1125                        }
1126
1127                        if (paramName == null) {
1128                                continue;
1129                        }
1130
1131                        try {
1132                                myFhirContext.getResourceDefinition(targetResourceType);
1133                        } catch (DataFormatException e) {
1134                                throw new InvalidRequestException(Msg.code(1208) + "Invalid resource type: " + targetResourceType);
1135                        }
1136
1137                        ArrayList<IQueryParameterType> orValues = Lists.newArrayList();
1138
1139                        if (paramName.startsWith("_has:")) {
1140
1141                                ourLog.trace("Handling double _has query: {}", paramName);
1142
1143                                String qualifier = paramName.substring(4);
1144                                for (IQueryParameterType next : nextOrList) {
1145                                        HasParam nextHasParam = new HasParam();
1146                                        nextHasParam.setValueAsQueryToken(
1147                                                        myFhirContext, PARAM_HAS, qualifier, next.getValueAsQueryToken(myFhirContext));
1148                                        orValues.add(nextHasParam);
1149                                }
1150
1151                        } else if (paramName.equals(PARAM_ID)) {
1152
1153                                for (IQueryParameterType next : nextOrList) {
1154                                        orValues.add(new TokenParam(next.getValueAsQueryToken(myFhirContext)));
1155                                }
1156
1157                        } else {
1158
1159                                // Ensure that the name of the search param
1160                                // (e.g. the `code` in Patient?_has:Observation:subject:code=sys|val)
1161                                // exists on the target resource type.
1162                                RuntimeSearchParam owningParameterDef =
1163                                                mySearchParamRegistry.getRuntimeSearchParam(targetResourceType, paramName);
1164
1165                                // Ensure that the name of the back-referenced search param on the target (e.g. the `subject` in
1166                                // Patient?_has:Observation:subject:code=sys|val)
1167                                // exists on the target resource, or in the top-level Resource resource.
1168                                mySearchParamRegistry.getRuntimeSearchParam(targetResourceType, paramReference);
1169
1170                                IQueryParameterAnd<?> parsedParam = JpaParamUtil.parseQueryParams(
1171                                                mySearchParamRegistry, myFhirContext, owningParameterDef, paramName, parameters);
1172
1173                                for (IQueryParameterOr<?> next : parsedParam.getValuesAsQueryTokens()) {
1174                                        orValues.addAll(next.getValuesAsQueryTokens());
1175                                }
1176                        }
1177
1178                        // Handle internal chain inside the has.
1179                        if (parameterName.contains(".")) {
1180                                // Previously, for some unknown reason, we were calling getChainedPart() twice.  This broke the _has
1181                                // then chain, then _has use case by effectively cutting off the second part of the chain and
1182                                // missing one iteration of the recursive call to build the query.
1183                                // So, for example, for
1184                                // Practitioner?_has:ExplanationOfBenefit:care-team:coverage.payor._has:List:item:_id=list1
1185                                // instead of passing " payor._has:List:item:_id=list1" to the next recursion, the second call to
1186                                // getChainedPart() was wrongly removing "payor." and passing down "_has:List:item:_id=list1" instead.
1187                                // This resulted in running incorrect SQL with nonsensical join that resulted in 0 results.
1188                                // However, after running the pipeline,  I've concluded there's no use case at all for the
1189                                // double call to "getChainedPart()", which is why there's no conditional logic at all to make a double
1190                                // call to getChainedPart().
1191                                final String chainedPart = getChainedPart(parameterName);
1192
1193                                orValues.stream()
1194                                                .filter(qp -> qp instanceof ReferenceParam)
1195                                                .map(qp -> (ReferenceParam) qp)
1196                                                .forEach(rp -> rp.setChain(chainedPart));
1197
1198                                parameterName = parameterName.substring(0, parameterName.indexOf('.'));
1199                        }
1200
1201                        int colonIndex = parameterName.indexOf(':');
1202                        if (colonIndex != -1) {
1203                                parameterName = parameterName.substring(0, colonIndex);
1204                        }
1205
1206                        ResourceLinkPredicateBuilder resourceLinkTableJoin =
1207                                        mySqlBuilder.addReferencePredicateBuilderReversed(this, theSourceJoinColumn);
1208                        Condition partitionPredicate = resourceLinkTableJoin.createPartitionIdPredicate(theRequestPartitionId);
1209
1210                        List<String> paths = resourceLinkTableJoin.createResourceLinkPaths(
1211                                        targetResourceType, paramReference, new ArrayList<>());
1212                        if (CollectionUtils.isEmpty(paths)) {
1213                                throw new InvalidRequestException(Msg.code(2305) + "Reference field does not exist: " + paramReference);
1214                        }
1215
1216                        Condition typePredicate = BinaryCondition.equalTo(
1217                                        resourceLinkTableJoin.getColumnTargetResourceType(),
1218                                        mySqlBuilder.generatePlaceholder(theResourceType));
1219                        Condition pathPredicate = toEqualToOrInPredicate(
1220                                        resourceLinkTableJoin.getColumnSourcePath(), mySqlBuilder.generatePlaceholders(paths));
1221
1222                        Condition linkedPredicate =
1223                                        searchForIdsWithAndOr(with().setSourceJoinColumn(resourceLinkTableJoin.getColumnSrcResourceId())
1224                                                        .setResourceName(targetResourceType)
1225                                                        .setParamName(parameterName)
1226                                                        .setAndOrParams(Collections.singletonList(orValues))
1227                                                        .setRequest(theRequest)
1228                                                        .setRequestPartitionId(theRequestPartitionId));
1229
1230                        andPredicates.add(toAndPredicate(partitionPredicate, pathPredicate, typePredicate, linkedPredicate));
1231                }
1232
1233                return toAndPredicate(andPredicates);
1234        }
1235
1236        public Condition createPredicateNumber(
1237                        @Nullable DbColumn theSourceJoinColumn,
1238                        String theResourceName,
1239                        String theSpnamePrefix,
1240                        RuntimeSearchParam theSearchParam,
1241                        List<? extends IQueryParameterType> theList,
1242                        SearchFilterParser.CompareOperation theOperation,
1243                        RequestPartitionId theRequestPartitionId) {
1244                return createPredicateNumber(
1245                                theSourceJoinColumn,
1246                                theResourceName,
1247                                theSpnamePrefix,
1248                                theSearchParam,
1249                                theList,
1250                                theOperation,
1251                                theRequestPartitionId,
1252                                mySqlBuilder);
1253        }
1254
1255        public Condition createPredicateNumber(
1256                        @Nullable DbColumn theSourceJoinColumn,
1257                        String theResourceName,
1258                        String theSpnamePrefix,
1259                        RuntimeSearchParam theSearchParam,
1260                        List<? extends IQueryParameterType> theList,
1261                        SearchFilterParser.CompareOperation theOperation,
1262                        RequestPartitionId theRequestPartitionId,
1263                        SearchQueryBuilder theSqlBuilder) {
1264
1265                String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
1266
1267                Boolean isMissing = theList.get(0).getMissing();
1268                if (isMissing != null) {
1269                        return createMissingParameterQuery(new MissingParameterQueryParams(
1270                                        theSqlBuilder,
1271                                        theSearchParam.getParamType(),
1272                                        theList,
1273                                        paramName,
1274                                        theResourceName,
1275                                        theSourceJoinColumn,
1276                                        theRequestPartitionId));
1277                } else {
1278                        NumberPredicateBuilder join = createOrReusePredicateBuilder(
1279                                                        PredicateBuilderTypeEnum.NUMBER,
1280                                                        theSourceJoinColumn,
1281                                                        paramName,
1282                                                        () -> theSqlBuilder.addNumberPredicateBuilder(theSourceJoinColumn))
1283                                        .getResult();
1284
1285                        List<Condition> codePredicates = new ArrayList<>();
1286                        for (IQueryParameterType nextOr : theList) {
1287
1288                                if (nextOr instanceof NumberParam) {
1289                                        NumberParam param = (NumberParam) nextOr;
1290
1291                                        BigDecimal value = param.getValue();
1292                                        if (value == null) {
1293                                                continue;
1294                                        }
1295
1296                                        SearchFilterParser.CompareOperation operation = theOperation;
1297                                        if (operation == null) {
1298                                                operation = toOperation(param.getPrefix());
1299                                        }
1300
1301                                        Condition predicate = join.createPredicateNumeric(
1302                                                        theResourceName, paramName, operation, value, theRequestPartitionId, nextOr);
1303                                        codePredicates.add(predicate);
1304
1305                                } else {
1306                                        throw new IllegalArgumentException(Msg.code(1211) + "Invalid token type: " + nextOr.getClass());
1307                                }
1308                        }
1309
1310                        return join.combineWithRequestPartitionIdPredicate(
1311                                        theRequestPartitionId, ComboCondition.or(codePredicates.toArray(new Condition[0])));
1312                }
1313        }
1314
1315        public Condition createPredicateQuantity(
1316                        @Nullable DbColumn theSourceJoinColumn,
1317                        String theResourceName,
1318                        String theSpnamePrefix,
1319                        RuntimeSearchParam theSearchParam,
1320                        List<? extends IQueryParameterType> theList,
1321                        SearchFilterParser.CompareOperation theOperation,
1322                        RequestPartitionId theRequestPartitionId) {
1323                return createPredicateQuantity(
1324                                theSourceJoinColumn,
1325                                theResourceName,
1326                                theSpnamePrefix,
1327                                theSearchParam,
1328                                theList,
1329                                theOperation,
1330                                theRequestPartitionId,
1331                                mySqlBuilder);
1332        }
1333
1334        public Condition createPredicateQuantity(
1335                        @Nullable DbColumn theSourceJoinColumn,
1336                        String theResourceName,
1337                        String theSpnamePrefix,
1338                        RuntimeSearchParam theSearchParam,
1339                        List<? extends IQueryParameterType> theList,
1340                        SearchFilterParser.CompareOperation theOperation,
1341                        RequestPartitionId theRequestPartitionId,
1342                        SearchQueryBuilder theSqlBuilder) {
1343
1344                String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
1345
1346                Boolean isMissing = theList.get(0).getMissing();
1347                if (isMissing != null) {
1348                        return createMissingParameterQuery(new MissingParameterQueryParams(
1349                                        theSqlBuilder,
1350                                        theSearchParam.getParamType(),
1351                                        theList,
1352                                        paramName,
1353                                        theResourceName,
1354                                        theSourceJoinColumn,
1355                                        theRequestPartitionId));
1356                } else {
1357                        List<QuantityParam> quantityParams =
1358                                        theList.stream().map(t -> QuantityParam.toQuantityParam(t)).collect(Collectors.toList());
1359
1360                        BaseQuantityPredicateBuilder join = null;
1361                        boolean normalizedSearchEnabled = myStorageSettings
1362                                        .getNormalizedQuantitySearchLevel()
1363                                        .equals(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_SUPPORTED);
1364                        if (normalizedSearchEnabled) {
1365                                List<QuantityParam> normalizedQuantityParams = quantityParams.stream()
1366                                                .map(t -> UcumServiceUtil.toCanonicalQuantityOrNull(t))
1367                                                .filter(t -> t != null)
1368                                                .collect(Collectors.toList());
1369
1370                                if (normalizedQuantityParams.size() == quantityParams.size()) {
1371                                        join = createOrReusePredicateBuilder(
1372                                                                        PredicateBuilderTypeEnum.QUANTITY,
1373                                                                        theSourceJoinColumn,
1374                                                                        paramName,
1375                                                                        () -> theSqlBuilder.addQuantityNormalizedPredicateBuilder(theSourceJoinColumn))
1376                                                        .getResult();
1377                                        quantityParams = normalizedQuantityParams;
1378                                }
1379                        }
1380
1381                        if (join == null) {
1382                                join = createOrReusePredicateBuilder(
1383                                                                PredicateBuilderTypeEnum.QUANTITY,
1384                                                                theSourceJoinColumn,
1385                                                                paramName,
1386                                                                () -> theSqlBuilder.addQuantityPredicateBuilder(theSourceJoinColumn))
1387                                                .getResult();
1388                        }
1389
1390                        List<Condition> codePredicates = new ArrayList<>();
1391                        for (QuantityParam nextOr : quantityParams) {
1392                                Condition singleCode = join.createPredicateQuantity(
1393                                                nextOr, theResourceName, paramName, null, join, theOperation, theRequestPartitionId);
1394                                codePredicates.add(singleCode);
1395                        }
1396
1397                        return join.combineWithRequestPartitionIdPredicate(
1398                                        theRequestPartitionId, ComboCondition.or(codePredicates.toArray(new Condition[0])));
1399                }
1400        }
1401
1402        public Condition createPredicateReference(
1403                        @Nullable DbColumn theSourceJoinColumn,
1404                        String theResourceName,
1405                        String theParamName,
1406                        List<String> theQualifiers,
1407                        List<? extends IQueryParameterType> theList,
1408                        SearchFilterParser.CompareOperation theOperation,
1409                        RequestDetails theRequest,
1410                        RequestPartitionId theRequestPartitionId) {
1411                return createPredicateReference(
1412                                theSourceJoinColumn,
1413                                theResourceName,
1414                                theParamName,
1415                                theQualifiers,
1416                                theList,
1417                                theOperation,
1418                                theRequest,
1419                                theRequestPartitionId,
1420                                mySqlBuilder);
1421        }
1422
1423        public Condition createPredicateReference(
1424                        @Nullable DbColumn theSourceJoinColumn,
1425                        String theResourceName,
1426                        String theParamName,
1427                        List<String> theQualifiers,
1428                        List<? extends IQueryParameterType> theList,
1429                        SearchFilterParser.CompareOperation theOperation,
1430                        RequestDetails theRequest,
1431                        RequestPartitionId theRequestPartitionId,
1432                        SearchQueryBuilder theSqlBuilder) {
1433
1434                if ((theOperation != null)
1435                                && (theOperation != SearchFilterParser.CompareOperation.eq)
1436                                && (theOperation != SearchFilterParser.CompareOperation.ne)) {
1437                        throw new InvalidRequestException(
1438                                        Msg.code(1212)
1439                                                        + "Invalid operator specified for reference predicate.  Supported operators for reference predicate are \"eq\" and \"ne\".");
1440                }
1441
1442                Boolean isMissing = theList.get(0).getMissing();
1443                if (isMissing != null) {
1444                        return createMissingParameterQuery(new MissingParameterQueryParams(
1445                                        theSqlBuilder,
1446                                        RestSearchParameterTypeEnum.REFERENCE,
1447                                        theList,
1448                                        theParamName,
1449                                        theResourceName,
1450                                        theSourceJoinColumn,
1451                                        theRequestPartitionId));
1452                } else {
1453                        ResourceLinkPredicateBuilder predicateBuilder = createOrReusePredicateBuilder(
1454                                                        PredicateBuilderTypeEnum.REFERENCE,
1455                                                        theSourceJoinColumn,
1456                                                        theParamName,
1457                                                        () -> theSqlBuilder.addReferencePredicateBuilder(this, theSourceJoinColumn))
1458                                        .getResult();
1459                        return predicateBuilder.createPredicate(
1460                                        theRequest,
1461                                        theResourceName,
1462                                        theParamName,
1463                                        theQualifiers,
1464                                        theList,
1465                                        theOperation,
1466                                        theRequestPartitionId);
1467                }
1468        }
1469
1470        public void addGrouping() {
1471                BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
1472                mySqlBuilder.getSelect().addGroupings(firstPredicateBuilder.getResourceIdColumn());
1473        }
1474
1475        public Condition createPredicateReferenceForEmbeddedChainedSearchResource(
1476                        @Nullable DbColumn theSourceJoinColumn,
1477                        String theResourceName,
1478                        RuntimeSearchParam theSearchParam,
1479                        List<? extends IQueryParameterType> theList,
1480                        SearchFilterParser.CompareOperation theOperation,
1481                        RequestDetails theRequest,
1482                        RequestPartitionId theRequestPartitionId,
1483                        EmbeddedChainedSearchModeEnum theEmbeddedChainedSearchModeEnum) {
1484
1485                boolean wantChainedAndNormal =
1486                                theEmbeddedChainedSearchModeEnum == EmbeddedChainedSearchModeEnum.UPLIFTED_AND_REF_JOIN;
1487
1488                // A bit of a hack, but we need to turn off cache reuse while in this method so that we don't try to reuse
1489                // builders across different subselects
1490                EnumSet<PredicateBuilderTypeEnum> cachedReusePredicateBuilderTypes =
1491                                EnumSet.copyOf(myReusePredicateBuilderTypes);
1492                if (wantChainedAndNormal) {
1493                        myReusePredicateBuilderTypes.clear();
1494                }
1495
1496                ReferenceChainExtractor chainExtractor = new ReferenceChainExtractor();
1497                chainExtractor.deriveChains(theResourceName, theSearchParam, theList);
1498                Map<List<ChainElement>, Set<LeafNodeDefinition>> chains = chainExtractor.getChains();
1499
1500                Map<List<String>, Set<LeafNodeDefinition>> referenceLinks = Maps.newHashMap();
1501                for (List<ChainElement> nextChain : chains.keySet()) {
1502                        Set<LeafNodeDefinition> leafNodes = chains.get(nextChain);
1503
1504                        collateChainedSearchOptions(referenceLinks, nextChain, leafNodes, theEmbeddedChainedSearchModeEnum);
1505                }
1506
1507                UnionQuery union = null;
1508                List<Condition> predicates = null;
1509                if (wantChainedAndNormal) {
1510                        union = new UnionQuery(SetOperationQuery.Type.UNION_ALL);
1511                } else {
1512                        predicates = new ArrayList<>();
1513                }
1514
1515                predicates = new ArrayList<>();
1516                for (List<String> nextReferenceLink : referenceLinks.keySet()) {
1517                        for (LeafNodeDefinition leafNodeDefinition : referenceLinks.get(nextReferenceLink)) {
1518                                SearchQueryBuilder builder;
1519                                if (wantChainedAndNormal) {
1520                                        builder = mySqlBuilder.newChildSqlBuilder();
1521                                } else {
1522                                        builder = mySqlBuilder;
1523                                }
1524
1525                                DbColumn previousJoinColumn = null;
1526
1527                                // Create a reference link predicates to the subselect for every link but the last one
1528                                for (String nextLink : nextReferenceLink) {
1529                                        // We don't want to call createPredicateReference() here, because the whole point is to avoid the
1530                                        // recursion.
1531                                        // TODO: Are we missing any important business logic from that method? All tests are passing.
1532                                        ResourceLinkPredicateBuilder resourceLinkPredicateBuilder =
1533                                                        builder.addReferencePredicateBuilder(this, previousJoinColumn);
1534                                        builder.addPredicate(
1535                                                        resourceLinkPredicateBuilder.createPredicateSourcePaths(Lists.newArrayList(nextLink)));
1536                                        previousJoinColumn = resourceLinkPredicateBuilder.getColumnTargetResourceId();
1537                                }
1538
1539                                Condition containedCondition = createIndexPredicate(
1540                                                previousJoinColumn,
1541                                                leafNodeDefinition.getLeafTarget(),
1542                                                leafNodeDefinition.getLeafPathPrefix(),
1543                                                leafNodeDefinition.getLeafParamName(),
1544                                                leafNodeDefinition.getParamDefinition(),
1545                                                leafNodeDefinition.getOrValues(),
1546                                                theOperation,
1547                                                leafNodeDefinition.getQualifiers(),
1548                                                theRequest,
1549                                                theRequestPartitionId,
1550                                                builder);
1551
1552                                if (wantChainedAndNormal) {
1553                                        builder.addPredicate(containedCondition);
1554                                        union.addQueries(builder.getSelect());
1555                                } else {
1556                                        predicates.add(containedCondition);
1557                                }
1558                        }
1559                }
1560
1561                Condition retVal;
1562                if (wantChainedAndNormal) {
1563
1564                        if (theSourceJoinColumn == null) {
1565                                retVal = new InCondition(
1566                                                mySqlBuilder.getOrCreateFirstPredicateBuilder(false).getResourceIdColumn(), union);
1567                        } else {
1568                                // -- for the resource link, need join with target_resource_id
1569                                retVal = new InCondition(theSourceJoinColumn, union);
1570                        }
1571
1572                } else {
1573
1574                        retVal = toOrPredicate(predicates);
1575                }
1576
1577                // restore the state of this collection to turn caching back on before we exit
1578                myReusePredicateBuilderTypes.addAll(cachedReusePredicateBuilderTypes);
1579                return retVal;
1580        }
1581
1582        private void collateChainedSearchOptions(
1583                        Map<List<String>, Set<LeafNodeDefinition>> referenceLinks,
1584                        List<ChainElement> nextChain,
1585                        Set<LeafNodeDefinition> leafNodes,
1586                        EmbeddedChainedSearchModeEnum theEmbeddedChainedSearchModeEnum) {
1587                // Manually collapse the chain using all possible variants of contained resource patterns.
1588                // This is a bit excruciating to extend beyond three references. Do we want to find a way to automate this
1589                // someday?
1590                // Note: the first element in each chain is assumed to be discrete. This may need to change when we add proper
1591                // support for `_contained`
1592                if (nextChain.size() == 1) {
1593                        // discrete -> discrete
1594                        if (theEmbeddedChainedSearchModeEnum == EmbeddedChainedSearchModeEnum.UPLIFTED_AND_REF_JOIN) {
1595                                // If !theWantChainedAndNormal that means we're only processing refchains
1596                                // so the discrete -> contained case is the only one that applies
1597                                updateMapOfReferenceLinks(
1598                                                referenceLinks, Lists.newArrayList(nextChain.get(0).getPath()), leafNodes);
1599                        }
1600
1601                        // discrete -> contained
1602                        RuntimeSearchParam firstParamDefinition =
1603                                        leafNodes.iterator().next().getParamDefinition();
1604                        updateMapOfReferenceLinks(
1605                                        referenceLinks,
1606                                        Lists.newArrayList(),
1607                                        leafNodes.stream()
1608                                                        .map(t -> t.withPathPrefix(
1609                                                                        nextChain.get(0).getResourceType(),
1610                                                                        nextChain.get(0).getSearchParameterName()))
1611                                                        // When we're handling discrete->contained the differences between search
1612                                                        // parameters don't matter. E.g. if we're processing "subject.name=foo"
1613                                                        // the name could be Patient:name or Group:name but it doesn't actually
1614                                                        // matter that these are different since in this case both of these end
1615                                                        // up being an identical search in the string table for "subject.name".
1616                                                        .map(t -> t.withParam(firstParamDefinition))
1617                                                        .collect(Collectors.toSet()));
1618                } else if (nextChain.size() == 2) {
1619                        // discrete -> discrete -> discrete
1620                        updateMapOfReferenceLinks(
1621                                        referenceLinks,
1622                                        Lists.newArrayList(
1623                                                        nextChain.get(0).getPath(), nextChain.get(1).getPath()),
1624                                        leafNodes);
1625                        // discrete -> discrete -> contained
1626                        updateMapOfReferenceLinks(
1627                                        referenceLinks,
1628                                        Lists.newArrayList(nextChain.get(0).getPath()),
1629                                        leafNodes.stream()
1630                                                        .map(t -> t.withPathPrefix(
1631                                                                        nextChain.get(1).getResourceType(),
1632                                                                        nextChain.get(1).getSearchParameterName()))
1633                                                        .collect(Collectors.toSet()));
1634                        // discrete -> contained -> discrete
1635                        updateMapOfReferenceLinks(
1636                                        referenceLinks,
1637                                        Lists.newArrayList(mergePaths(
1638                                                        nextChain.get(0).getPath(), nextChain.get(1).getPath())),
1639                                        leafNodes);
1640                        if (myStorageSettings.isIndexOnContainedResourcesRecursively()) {
1641                                // discrete -> contained -> contained
1642                                updateMapOfReferenceLinks(
1643                                                referenceLinks,
1644                                                Lists.newArrayList(),
1645                                                leafNodes.stream()
1646                                                                .map(t -> t.withPathPrefix(
1647                                                                                nextChain.get(0).getResourceType(),
1648                                                                                nextChain.get(0).getSearchParameterName() + "."
1649                                                                                                + nextChain.get(1).getSearchParameterName()))
1650                                                                .collect(Collectors.toSet()));
1651                        }
1652                } else if (nextChain.size() == 3) {
1653                        // discrete -> discrete -> discrete -> discrete
1654                        updateMapOfReferenceLinks(
1655                                        referenceLinks,
1656                                        Lists.newArrayList(
1657                                                        nextChain.get(0).getPath(),
1658                                                        nextChain.get(1).getPath(),
1659                                                        nextChain.get(2).getPath()),
1660                                        leafNodes);
1661                        // discrete -> discrete -> discrete -> contained
1662                        updateMapOfReferenceLinks(
1663                                        referenceLinks,
1664                                        Lists.newArrayList(
1665                                                        nextChain.get(0).getPath(), nextChain.get(1).getPath()),
1666                                        leafNodes.stream()
1667                                                        .map(t -> t.withPathPrefix(
1668                                                                        nextChain.get(2).getResourceType(),
1669                                                                        nextChain.get(2).getSearchParameterName()))
1670                                                        .collect(Collectors.toSet()));
1671                        // discrete -> discrete -> contained -> discrete
1672                        updateMapOfReferenceLinks(
1673                                        referenceLinks,
1674                                        Lists.newArrayList(
1675                                                        nextChain.get(0).getPath(),
1676                                                        mergePaths(
1677                                                                        nextChain.get(1).getPath(), nextChain.get(2).getPath())),
1678                                        leafNodes);
1679                        // discrete -> contained -> discrete -> discrete
1680                        updateMapOfReferenceLinks(
1681                                        referenceLinks,
1682                                        Lists.newArrayList(
1683                                                        mergePaths(
1684                                                                        nextChain.get(0).getPath(), nextChain.get(1).getPath()),
1685                                                        nextChain.get(2).getPath()),
1686                                        leafNodes);
1687                        // discrete -> contained -> discrete -> contained
1688                        updateMapOfReferenceLinks(
1689                                        referenceLinks,
1690                                        Lists.newArrayList(mergePaths(
1691                                                        nextChain.get(0).getPath(), nextChain.get(1).getPath())),
1692                                        leafNodes.stream()
1693                                                        .map(t -> t.withPathPrefix(
1694                                                                        nextChain.get(2).getResourceType(),
1695                                                                        nextChain.get(2).getSearchParameterName()))
1696                                                        .collect(Collectors.toSet()));
1697                        if (myStorageSettings.isIndexOnContainedResourcesRecursively()) {
1698                                // discrete -> contained -> contained -> discrete
1699                                updateMapOfReferenceLinks(
1700                                                referenceLinks,
1701                                                Lists.newArrayList(mergePaths(
1702                                                                nextChain.get(0).getPath(),
1703                                                                nextChain.get(1).getPath(),
1704                                                                nextChain.get(2).getPath())),
1705                                                leafNodes);
1706                                // discrete -> discrete -> contained -> contained
1707                                updateMapOfReferenceLinks(
1708                                                referenceLinks,
1709                                                Lists.newArrayList(nextChain.get(0).getPath()),
1710                                                leafNodes.stream()
1711                                                                .map(t -> t.withPathPrefix(
1712                                                                                nextChain.get(1).getResourceType(),
1713                                                                                nextChain.get(1).getSearchParameterName() + "."
1714                                                                                                + nextChain.get(2).getSearchParameterName()))
1715                                                                .collect(Collectors.toSet()));
1716                                // discrete -> contained -> contained -> contained
1717                                updateMapOfReferenceLinks(
1718                                                referenceLinks,
1719                                                Lists.newArrayList(),
1720                                                leafNodes.stream()
1721                                                                .map(t -> t.withPathPrefix(
1722                                                                                nextChain.get(0).getResourceType(),
1723                                                                                nextChain.get(0).getSearchParameterName() + "."
1724                                                                                                + nextChain.get(1).getSearchParameterName() + "."
1725                                                                                                + nextChain.get(2).getSearchParameterName()))
1726                                                                .collect(Collectors.toSet()));
1727                        }
1728                } else {
1729                        // TODO: the chain is too long, it isn't practical to hard-code all the possible patterns. If anyone ever
1730                        // needs this, we should revisit the approach
1731                        throw new InvalidRequestException(Msg.code(2011)
1732                                        + "The search chain is too long. Only chains of up to three references are supported.");
1733                }
1734        }
1735
1736        private void updateMapOfReferenceLinks(
1737                        Map<List<String>, Set<LeafNodeDefinition>> theReferenceLinksMap,
1738                        ArrayList<String> thePath,
1739                        Set<LeafNodeDefinition> theLeafNodesToAdd) {
1740                Set<LeafNodeDefinition> leafNodes = theReferenceLinksMap.get(thePath);
1741                if (leafNodes == null) {
1742                        leafNodes = Sets.newHashSet();
1743                        theReferenceLinksMap.put(thePath, leafNodes);
1744                }
1745                leafNodes.addAll(theLeafNodesToAdd);
1746        }
1747
1748        private String mergePaths(String... paths) {
1749                String result = "";
1750                for (String nextPath : paths) {
1751                        int separatorIndex = nextPath.indexOf('.');
1752                        if (StringUtils.isEmpty(result)) {
1753                                result = nextPath;
1754                        } else {
1755                                result = result + nextPath.substring(separatorIndex);
1756                        }
1757                }
1758                return result;
1759        }
1760
1761        private Condition createIndexPredicate(
1762                        DbColumn theSourceJoinColumn,
1763                        String theResourceName,
1764                        String theSpnamePrefix,
1765                        String theParamName,
1766                        RuntimeSearchParam theParamDefinition,
1767                        ArrayList<IQueryParameterType> theOrValues,
1768                        SearchFilterParser.CompareOperation theOperation,
1769                        List<String> theQualifiers,
1770                        RequestDetails theRequest,
1771                        RequestPartitionId theRequestPartitionId,
1772                        SearchQueryBuilder theSqlBuilder) {
1773                Condition containedCondition;
1774
1775                switch (theParamDefinition.getParamType()) {
1776                        case DATE:
1777                                containedCondition = createPredicateDate(
1778                                                theSourceJoinColumn,
1779                                                theResourceName,
1780                                                theSpnamePrefix,
1781                                                theParamDefinition,
1782                                                theOrValues,
1783                                                theOperation,
1784                                                theRequestPartitionId,
1785                                                theSqlBuilder);
1786                                break;
1787                        case NUMBER:
1788                                containedCondition = createPredicateNumber(
1789                                                theSourceJoinColumn,
1790                                                theResourceName,
1791                                                theSpnamePrefix,
1792                                                theParamDefinition,
1793                                                theOrValues,
1794                                                theOperation,
1795                                                theRequestPartitionId,
1796                                                theSqlBuilder);
1797                                break;
1798                        case QUANTITY:
1799                                containedCondition = createPredicateQuantity(
1800                                                theSourceJoinColumn,
1801                                                theResourceName,
1802                                                theSpnamePrefix,
1803                                                theParamDefinition,
1804                                                theOrValues,
1805                                                theOperation,
1806                                                theRequestPartitionId,
1807                                                theSqlBuilder);
1808                                break;
1809                        case STRING:
1810                                containedCondition = createPredicateString(
1811                                                theSourceJoinColumn,
1812                                                theResourceName,
1813                                                theSpnamePrefix,
1814                                                theParamDefinition,
1815                                                theOrValues,
1816                                                theOperation,
1817                                                theRequestPartitionId,
1818                                                theSqlBuilder);
1819                                break;
1820                        case TOKEN:
1821                                containedCondition = createPredicateToken(
1822                                                theSourceJoinColumn,
1823                                                theResourceName,
1824                                                theSpnamePrefix,
1825                                                theParamDefinition,
1826                                                theOrValues,
1827                                                theOperation,
1828                                                theRequestPartitionId,
1829                                                theSqlBuilder);
1830                                break;
1831                        case COMPOSITE:
1832                                containedCondition = createPredicateComposite(
1833                                                theSourceJoinColumn,
1834                                                theResourceName,
1835                                                theSpnamePrefix,
1836                                                theParamDefinition,
1837                                                theOrValues,
1838                                                theRequestPartitionId,
1839                                                theSqlBuilder);
1840                                break;
1841                        case URI:
1842                                containedCondition = createPredicateUri(
1843                                                theSourceJoinColumn,
1844                                                theResourceName,
1845                                                theSpnamePrefix,
1846                                                theParamDefinition,
1847                                                theOrValues,
1848                                                theOperation,
1849                                                theRequest,
1850                                                theRequestPartitionId,
1851                                                theSqlBuilder);
1852                                break;
1853                        case REFERENCE:
1854                                containedCondition = createPredicateReference(
1855                                                theSourceJoinColumn,
1856                                                theResourceName,
1857                                                isBlank(theSpnamePrefix) ? theParamName : theSpnamePrefix + "." + theParamName,
1858                                                theQualifiers,
1859                                                theOrValues,
1860                                                theOperation,
1861                                                theRequest,
1862                                                theRequestPartitionId,
1863                                                theSqlBuilder);
1864                                break;
1865                        case HAS:
1866                        case SPECIAL:
1867                        default:
1868                                throw new InvalidRequestException(
1869                                                Msg.code(1215) + "The search type:" + theParamDefinition.getParamType() + " is not supported.");
1870                }
1871                return containedCondition;
1872        }
1873
1874        @Nullable
1875        public Condition createPredicateResourceId(
1876                        @Nullable DbColumn theSourceJoinColumn,
1877                        List<List<IQueryParameterType>> theValues,
1878                        String theResourceName,
1879                        SearchFilterParser.CompareOperation theOperation,
1880                        RequestPartitionId theRequestPartitionId) {
1881                ResourceIdPredicateBuilder builder = mySqlBuilder.newResourceIdBuilder();
1882                return builder.createPredicateResourceId(
1883                                theSourceJoinColumn, theResourceName, theValues, theOperation, theRequestPartitionId);
1884        }
1885
1886        private Condition createPredicateSourceForAndList(
1887                        @Nullable DbColumn theSourceJoinColumn, List<List<IQueryParameterType>> theAndOrParams) {
1888                mySqlBuilder.getOrCreateFirstPredicateBuilder();
1889
1890                List<Condition> andPredicates = new ArrayList<>(theAndOrParams.size());
1891                for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
1892                        andPredicates.add(createPredicateSource(theSourceJoinColumn, nextAnd));
1893                }
1894                return toAndPredicate(andPredicates);
1895        }
1896
1897        private Condition createPredicateSource(
1898                        @Nullable DbColumn theSourceJoinColumn, List<? extends IQueryParameterType> theList) {
1899                if (myStorageSettings.getStoreMetaSourceInformation()
1900                                == JpaStorageSettings.StoreMetaSourceInformationEnum.NONE) {
1901                        String msg = myFhirContext.getLocalizer().getMessage(QueryStack.class, "sourceParamDisabled");
1902                        throw new InvalidRequestException(Msg.code(1216) + msg);
1903                }
1904
1905                List<Condition> orPredicates = new ArrayList<>();
1906
1907                // :missing=true modifier processing requires "LEFT JOIN" with HFJ_RESOURCE table to return correct results
1908                // if both sourceUri and requestId are not populated for the resource
1909                Optional<? extends IQueryParameterType> isMissingSourceOptional = theList.stream()
1910                                .filter(nextParameter -> nextParameter.getMissing() != null && nextParameter.getMissing())
1911                                .findFirst();
1912
1913                if (isMissingSourceOptional.isPresent()) {
1914                        SourcePredicateBuilder join =
1915                                        getSourcePredicateBuilder(theSourceJoinColumn, SelectQuery.JoinType.LEFT_OUTER);
1916                        orPredicates.add(join.createPredicateMissingSourceUri());
1917                        return toOrPredicate(orPredicates);
1918                }
1919                // for all other cases we use "INNER JOIN" to match search parameters
1920                SourcePredicateBuilder join = getSourcePredicateBuilder(theSourceJoinColumn, SelectQuery.JoinType.INNER);
1921
1922                for (IQueryParameterType nextParameter : theList) {
1923                        SourceParam sourceParameter = new SourceParam(nextParameter.getValueAsQueryToken(myFhirContext));
1924                        String sourceUri = sourceParameter.getSourceUri();
1925                        String requestId = sourceParameter.getRequestId();
1926                        if (isNotBlank(sourceUri) && isNotBlank(requestId)) {
1927                                orPredicates.add(toAndPredicate(
1928                                                join.createPredicateSourceUri(sourceUri), join.createPredicateRequestId(requestId)));
1929                        } else if (isNotBlank(sourceUri)) {
1930                                orPredicates.add(
1931                                                join.createPredicateSourceUriWithModifiers(nextParameter, myStorageSettings, sourceUri));
1932                        } else if (isNotBlank(requestId)) {
1933                                orPredicates.add(join.createPredicateRequestId(requestId));
1934                        }
1935                }
1936
1937                return toOrPredicate(orPredicates);
1938        }
1939
1940        private SourcePredicateBuilder getSourcePredicateBuilder(
1941                        @Nullable DbColumn theSourceJoinColumn, SelectQuery.JoinType theJoinType) {
1942                return createOrReusePredicateBuilder(
1943                                                PredicateBuilderTypeEnum.SOURCE,
1944                                                theSourceJoinColumn,
1945                                                Constants.PARAM_SOURCE,
1946                                                () -> mySqlBuilder.addSourcePredicateBuilder(theSourceJoinColumn, theJoinType))
1947                                .getResult();
1948        }
1949
1950        public Condition createPredicateString(
1951                        @Nullable DbColumn theSourceJoinColumn,
1952                        String theResourceName,
1953                        String theSpnamePrefix,
1954                        RuntimeSearchParam theSearchParam,
1955                        List<? extends IQueryParameterType> theList,
1956                        SearchFilterParser.CompareOperation theOperation,
1957                        RequestPartitionId theRequestPartitionId) {
1958                return createPredicateString(
1959                                theSourceJoinColumn,
1960                                theResourceName,
1961                                theSpnamePrefix,
1962                                theSearchParam,
1963                                theList,
1964                                theOperation,
1965                                theRequestPartitionId,
1966                                mySqlBuilder);
1967        }
1968
1969        public Condition createPredicateString(
1970                        @Nullable DbColumn theSourceJoinColumn,
1971                        String theResourceName,
1972                        String theSpnamePrefix,
1973                        RuntimeSearchParam theSearchParam,
1974                        List<? extends IQueryParameterType> theList,
1975                        SearchFilterParser.CompareOperation theOperation,
1976                        RequestPartitionId theRequestPartitionId,
1977                        SearchQueryBuilder theSqlBuilder) {
1978                Boolean isMissing = theList.get(0).getMissing();
1979                String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
1980
1981                if (isMissing != null) {
1982                        return createMissingParameterQuery(new MissingParameterQueryParams(
1983                                        theSqlBuilder,
1984                                        theSearchParam.getParamType(),
1985                                        theList,
1986                                        paramName,
1987                                        theResourceName,
1988                                        theSourceJoinColumn,
1989                                        theRequestPartitionId));
1990                }
1991
1992                StringPredicateBuilder join = createOrReusePredicateBuilder(
1993                                                PredicateBuilderTypeEnum.STRING,
1994                                                theSourceJoinColumn,
1995                                                paramName,
1996                                                () -> theSqlBuilder.addStringPredicateBuilder(theSourceJoinColumn))
1997                                .getResult();
1998
1999                List<Condition> codePredicates = new ArrayList<>();
2000                for (IQueryParameterType nextOr : theList) {
2001                        Condition singleCode = join.createPredicateString(
2002                                        nextOr, theResourceName, theSpnamePrefix, theSearchParam, join, theOperation);
2003                        codePredicates.add(singleCode);
2004                }
2005
2006                return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, toOrPredicate(codePredicates));
2007        }
2008
2009        public Condition createPredicateTag(
2010                        @Nullable DbColumn theSourceJoinColumn,
2011                        List<List<IQueryParameterType>> theList,
2012                        String theParamName,
2013                        RequestPartitionId theRequestPartitionId) {
2014                TagTypeEnum tagType;
2015                if (Constants.PARAM_TAG.equals(theParamName)) {
2016                        tagType = TagTypeEnum.TAG;
2017                } else if (Constants.PARAM_PROFILE.equals(theParamName)) {
2018                        tagType = TagTypeEnum.PROFILE;
2019                } else if (Constants.PARAM_SECURITY.equals(theParamName)) {
2020                        tagType = TagTypeEnum.SECURITY_LABEL;
2021                } else {
2022                        throw new IllegalArgumentException(Msg.code(1217) + "Param name: " + theParamName); // shouldn't happen
2023                }
2024
2025                List<Condition> andPredicates = new ArrayList<>();
2026                for (List<? extends IQueryParameterType> nextAndParams : theList) {
2027                        if (!checkHaveTags(nextAndParams, theParamName)) {
2028                                continue;
2029                        }
2030
2031                        List<Triple<String, String, String>> tokens = Lists.newArrayList();
2032                        boolean paramInverted = populateTokens(tokens, nextAndParams);
2033                        if (tokens.isEmpty()) {
2034                                continue;
2035                        }
2036
2037                        Condition tagPredicate;
2038                        BaseJoiningPredicateBuilder join;
2039                        if (paramInverted) {
2040
2041                                SearchQueryBuilder sqlBuilder = mySqlBuilder.newChildSqlBuilder();
2042                                TagPredicateBuilder tagSelector = sqlBuilder.addTagPredicateBuilder(null);
2043                                sqlBuilder.addPredicate(
2044                                                tagSelector.createPredicateTag(tagType, tokens, theParamName, theRequestPartitionId));
2045                                SelectQuery sql = sqlBuilder.getSelect();
2046
2047                                join = mySqlBuilder.getOrCreateFirstPredicateBuilder();
2048                                Expression subSelect = new Subquery(sql);
2049                                tagPredicate = new InCondition(join.getResourceIdColumn(), subSelect).setNegate(true);
2050
2051                        } else {
2052                                // Tag table can't be a query root because it will include deleted resources, and can't select by
2053                                // resource type
2054                                mySqlBuilder.getOrCreateFirstPredicateBuilder();
2055
2056                                TagPredicateBuilder tagJoin = createOrReusePredicateBuilder(
2057                                                                PredicateBuilderTypeEnum.TAG,
2058                                                                theSourceJoinColumn,
2059                                                                theParamName,
2060                                                                () -> mySqlBuilder.addTagPredicateBuilder(theSourceJoinColumn))
2061                                                .getResult();
2062                                tagPredicate = tagJoin.createPredicateTag(tagType, tokens, theParamName, theRequestPartitionId);
2063                                join = tagJoin;
2064                        }
2065
2066                        andPredicates.add(join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, tagPredicate));
2067                }
2068
2069                return toAndPredicate(andPredicates);
2070        }
2071
2072        private boolean populateTokens(
2073                        List<Triple<String, String, String>> theTokens, List<? extends IQueryParameterType> theAndParams) {
2074                boolean paramInverted = false;
2075
2076                for (IQueryParameterType nextOrParam : theAndParams) {
2077                        String code;
2078                        String system;
2079                        if (nextOrParam instanceof TokenParam) {
2080                                TokenParam nextParam = (TokenParam) nextOrParam;
2081                                code = nextParam.getValue();
2082                                system = nextParam.getSystem();
2083                                if (nextParam.getModifier() == TokenParamModifier.NOT) {
2084                                        paramInverted = true;
2085                                }
2086                        } else {
2087                                UriParam nextParam = (UriParam) nextOrParam;
2088                                code = nextParam.getValue();
2089                                system = null;
2090                        }
2091
2092                        if (isNotBlank(code)) {
2093                                theTokens.add(Triple.of(system, nextOrParam.getQueryParameterQualifier(), code));
2094                        }
2095                }
2096                return paramInverted;
2097        }
2098
2099        private boolean checkHaveTags(List<? extends IQueryParameterType> theParams, String theParamName) {
2100                for (IQueryParameterType nextParamUncasted : theParams) {
2101                        if (nextParamUncasted instanceof TokenParam) {
2102                                TokenParam nextParam = (TokenParam) nextParamUncasted;
2103                                if (isNotBlank(nextParam.getValue())) {
2104                                        return true;
2105                                }
2106                                if (isNotBlank(nextParam.getSystem())) {
2107                                        throw new TokenParamFormatInvalidRequestException(
2108                                                        Msg.code(1218), theParamName, nextParam.getValueAsQueryToken(myFhirContext));
2109                                }
2110                        }
2111
2112                        UriParam nextParam = (UriParam) nextParamUncasted;
2113                        if (isNotBlank(nextParam.getValue())) {
2114                                return true;
2115                        }
2116                }
2117
2118                return false;
2119        }
2120
2121        public Condition createPredicateToken(
2122                        @Nullable DbColumn theSourceJoinColumn,
2123                        String theResourceName,
2124                        String theSpnamePrefix,
2125                        RuntimeSearchParam theSearchParam,
2126                        List<? extends IQueryParameterType> theList,
2127                        SearchFilterParser.CompareOperation theOperation,
2128                        RequestPartitionId theRequestPartitionId) {
2129                return createPredicateToken(
2130                                theSourceJoinColumn,
2131                                theResourceName,
2132                                theSpnamePrefix,
2133                                theSearchParam,
2134                                theList,
2135                                theOperation,
2136                                theRequestPartitionId,
2137                                mySqlBuilder);
2138        }
2139
2140        public Condition createPredicateToken(
2141                        @Nullable DbColumn theSourceJoinColumn,
2142                        String theResourceName,
2143                        String theSpnamePrefix,
2144                        RuntimeSearchParam theSearchParam,
2145                        List<? extends IQueryParameterType> theList,
2146                        SearchFilterParser.CompareOperation theOperation,
2147                        RequestPartitionId theRequestPartitionId,
2148                        SearchQueryBuilder theSqlBuilder) {
2149
2150                List<IQueryParameterType> tokens = new ArrayList<>();
2151
2152                boolean paramInverted = false;
2153                TokenParamModifier modifier;
2154
2155                for (IQueryParameterType nextOr : theList) {
2156                        if (nextOr instanceof TokenParam) {
2157                                if (!((TokenParam) nextOr).isEmpty()) {
2158                                        TokenParam id = (TokenParam) nextOr;
2159                                        if (id.isText()) {
2160
2161                                                // Check whether the :text modifier is actually enabled here
2162                                                boolean tokenTextIndexingEnabled =
2163                                                                BaseSearchParamExtractor.tokenTextIndexingEnabledForSearchParam(
2164                                                                                myStorageSettings, theSearchParam);
2165                                                if (!tokenTextIndexingEnabled) {
2166                                                        String msg;
2167                                                        if (myStorageSettings.isSuppressStringIndexingInTokens()) {
2168                                                                msg = myFhirContext
2169                                                                                .getLocalizer()
2170                                                                                .getMessage(QueryStack.class, "textModifierDisabledForServer");
2171                                                        } else {
2172                                                                msg = myFhirContext
2173                                                                                .getLocalizer()
2174                                                                                .getMessage(QueryStack.class, "textModifierDisabledForSearchParam");
2175                                                        }
2176                                                        throw new MethodNotAllowedException(Msg.code(1219) + msg);
2177                                                }
2178                                                return createPredicateString(
2179                                                                theSourceJoinColumn,
2180                                                                theResourceName,
2181                                                                theSpnamePrefix,
2182                                                                theSearchParam,
2183                                                                theList,
2184                                                                null,
2185                                                                theRequestPartitionId,
2186                                                                theSqlBuilder);
2187                                        }
2188
2189                                        modifier = id.getModifier();
2190                                        // for :not modifier, create a token and remove the :not modifier
2191                                        if (modifier == TokenParamModifier.NOT) {
2192                                                tokens.add(new TokenParam(((TokenParam) nextOr).getSystem(), ((TokenParam) nextOr).getValue()));
2193                                                paramInverted = true;
2194                                        } else {
2195                                                tokens.add(nextOr);
2196                                        }
2197                                }
2198                        } else {
2199                                tokens.add(nextOr);
2200                        }
2201                }
2202
2203                if (tokens.isEmpty()) {
2204                        return null;
2205                }
2206
2207                String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
2208                Condition predicate;
2209                BaseJoiningPredicateBuilder join;
2210
2211                if (paramInverted) {
2212                        SearchQueryBuilder sqlBuilder = theSqlBuilder.newChildSqlBuilder();
2213                        TokenPredicateBuilder tokenSelector = sqlBuilder.addTokenPredicateBuilder(null);
2214                        sqlBuilder.addPredicate(tokenSelector.createPredicateToken(
2215                                        tokens, theResourceName, theSpnamePrefix, theSearchParam, theRequestPartitionId));
2216                        SelectQuery sql = sqlBuilder.getSelect();
2217                        Expression subSelect = new Subquery(sql);
2218
2219                        join = theSqlBuilder.getOrCreateFirstPredicateBuilder();
2220
2221                        if (theSourceJoinColumn == null) {
2222                                predicate = new InCondition(join.getResourceIdColumn(), subSelect).setNegate(true);
2223                        } else {
2224                                // -- for the resource link, need join with target_resource_id
2225                                predicate = new InCondition(theSourceJoinColumn, subSelect).setNegate(true);
2226                        }
2227
2228                } else {
2229                        Boolean isMissing = theList.get(0).getMissing();
2230                        if (isMissing != null) {
2231                                return createMissingParameterQuery(new MissingParameterQueryParams(
2232                                                theSqlBuilder,
2233                                                theSearchParam.getParamType(),
2234                                                theList,
2235                                                paramName,
2236                                                theResourceName,
2237                                                theSourceJoinColumn,
2238                                                theRequestPartitionId));
2239                        }
2240
2241                        TokenPredicateBuilder tokenJoin = createOrReusePredicateBuilder(
2242                                                        PredicateBuilderTypeEnum.TOKEN,
2243                                                        theSourceJoinColumn,
2244                                                        paramName,
2245                                                        () -> theSqlBuilder.addTokenPredicateBuilder(theSourceJoinColumn))
2246                                        .getResult();
2247
2248                        predicate = tokenJoin.createPredicateToken(
2249                                        tokens, theResourceName, theSpnamePrefix, theSearchParam, theOperation, theRequestPartitionId);
2250                        join = tokenJoin;
2251                }
2252
2253                return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate);
2254        }
2255
2256        public Condition createPredicateUri(
2257                        @Nullable DbColumn theSourceJoinColumn,
2258                        String theResourceName,
2259                        String theSpnamePrefix,
2260                        RuntimeSearchParam theSearchParam,
2261                        List<? extends IQueryParameterType> theList,
2262                        SearchFilterParser.CompareOperation theOperation,
2263                        RequestDetails theRequestDetails,
2264                        RequestPartitionId theRequestPartitionId) {
2265                return createPredicateUri(
2266                                theSourceJoinColumn,
2267                                theResourceName,
2268                                theSpnamePrefix,
2269                                theSearchParam,
2270                                theList,
2271                                theOperation,
2272                                theRequestDetails,
2273                                theRequestPartitionId,
2274                                mySqlBuilder);
2275        }
2276
2277        public Condition createPredicateUri(
2278                        @Nullable DbColumn theSourceJoinColumn,
2279                        String theResourceName,
2280                        String theSpnamePrefix,
2281                        RuntimeSearchParam theSearchParam,
2282                        List<? extends IQueryParameterType> theList,
2283                        SearchFilterParser.CompareOperation theOperation,
2284                        RequestDetails theRequestDetails,
2285                        RequestPartitionId theRequestPartitionId,
2286                        SearchQueryBuilder theSqlBuilder) {
2287
2288                String paramName = getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
2289
2290                Boolean isMissing = theList.get(0).getMissing();
2291                if (isMissing != null) {
2292                        return createMissingParameterQuery(new MissingParameterQueryParams(
2293                                        theSqlBuilder,
2294                                        theSearchParam.getParamType(),
2295                                        theList,
2296                                        paramName,
2297                                        theResourceName,
2298                                        theSourceJoinColumn,
2299                                        theRequestPartitionId));
2300                } else {
2301                        UriPredicateBuilder join = theSqlBuilder.addUriPredicateBuilder(theSourceJoinColumn);
2302
2303                        Condition predicate = join.addPredicate(theList, paramName, theOperation, theRequestDetails);
2304                        return join.combineWithRequestPartitionIdPredicate(theRequestPartitionId, predicate);
2305                }
2306        }
2307
2308        public QueryStack newChildQueryFactoryWithFullBuilderReuse() {
2309                return new QueryStack(
2310                                mySearchParameters,
2311                                myStorageSettings,
2312                                myFhirContext,
2313                                mySqlBuilder,
2314                                mySearchParamRegistry,
2315                                myPartitionSettings,
2316                                EnumSet.allOf(PredicateBuilderTypeEnum.class));
2317        }
2318
2319        @Nullable
2320        public Condition searchForIdsWithAndOr(SearchForIdsParams theSearchForIdsParams) {
2321
2322                if (theSearchForIdsParams.myAndOrParams.isEmpty()) {
2323                        return null;
2324                }
2325
2326                switch (theSearchForIdsParams.myParamName) {
2327                        case IAnyResource.SP_RES_ID:
2328                                return createPredicateResourceId(
2329                                                theSearchForIdsParams.mySourceJoinColumn,
2330                                                theSearchForIdsParams.myAndOrParams,
2331                                                theSearchForIdsParams.myResourceName,
2332                                                null,
2333                                                theSearchForIdsParams.myRequestPartitionId);
2334
2335                        case Constants.PARAM_PID:
2336                                return createPredicateResourcePID(
2337                                                theSearchForIdsParams.mySourceJoinColumn, theSearchForIdsParams.myAndOrParams);
2338
2339                        case PARAM_HAS:
2340                                return createPredicateHas(
2341                                                theSearchForIdsParams.mySourceJoinColumn,
2342                                                theSearchForIdsParams.myResourceName,
2343                                                theSearchForIdsParams.myAndOrParams,
2344                                                theSearchForIdsParams.myRequest,
2345                                                theSearchForIdsParams.myRequestPartitionId);
2346
2347                        case Constants.PARAM_TAG:
2348                        case Constants.PARAM_PROFILE:
2349                        case Constants.PARAM_SECURITY:
2350                                if (myStorageSettings.getTagStorageMode() == JpaStorageSettings.TagStorageModeEnum.INLINE) {
2351                                        return createPredicateSearchParameter(
2352                                                        theSearchForIdsParams.mySourceJoinColumn,
2353                                                        theSearchForIdsParams.myResourceName,
2354                                                        theSearchForIdsParams.myParamName,
2355                                                        theSearchForIdsParams.myAndOrParams,
2356                                                        theSearchForIdsParams.myRequest,
2357                                                        theSearchForIdsParams.myRequestPartitionId);
2358                                } else {
2359                                        return createPredicateTag(
2360                                                        theSearchForIdsParams.mySourceJoinColumn,
2361                                                        theSearchForIdsParams.myAndOrParams,
2362                                                        theSearchForIdsParams.myParamName,
2363                                                        theSearchForIdsParams.myRequestPartitionId);
2364                                }
2365
2366                        case Constants.PARAM_SOURCE:
2367                                return createPredicateSourceForAndList(
2368                                                theSearchForIdsParams.mySourceJoinColumn, theSearchForIdsParams.myAndOrParams);
2369
2370                        case Constants.PARAM_LASTUPDATED:
2371                                // this case statement handles a _lastUpdated query as part of a reverse search
2372                                // only (/Patient?_has:Encounter:patient:_lastUpdated=ge2023-10-24).
2373                                // performing a _lastUpdated query on a resource (/Patient?_lastUpdated=eq2023-10-24)
2374                                // is handled in {@link SearchBuilder#createChunkedQuery}.
2375                                return createReverseSearchPredicateLastUpdated(
2376                                                theSearchForIdsParams.myAndOrParams, theSearchForIdsParams.mySourceJoinColumn);
2377
2378                        default:
2379                                return createPredicateSearchParameter(
2380                                                theSearchForIdsParams.mySourceJoinColumn,
2381                                                theSearchForIdsParams.myResourceName,
2382                                                theSearchForIdsParams.myParamName,
2383                                                theSearchForIdsParams.myAndOrParams,
2384                                                theSearchForIdsParams.myRequest,
2385                                                theSearchForIdsParams.myRequestPartitionId);
2386                }
2387        }
2388
2389        /**
2390         * Raw match on RES_ID
2391         */
2392        private Condition createPredicateResourcePID(
2393                        DbColumn theSourceJoinColumn, List<List<IQueryParameterType>> theAndOrParams) {
2394
2395                DbColumn pidColumn = theSourceJoinColumn;
2396
2397                if (pidColumn == null) {
2398                        BaseJoiningPredicateBuilder predicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
2399                        pidColumn = predicateBuilder.getResourceIdColumn();
2400                }
2401
2402                // we don't support any modifiers for now
2403                Set<Long> pids = theAndOrParams.stream()
2404                                .map(orList -> orList.stream()
2405                                                .map(v -> v.getValueAsQueryToken(myFhirContext))
2406                                                .map(Long::valueOf)
2407                                                .collect(Collectors.toSet()))
2408                                .reduce(Sets::intersection)
2409                                .orElse(Set.of());
2410
2411                if (pids.isEmpty()) {
2412                        mySqlBuilder.setMatchNothing();
2413                        return null;
2414                }
2415
2416                return toEqualToOrInPredicate(pidColumn, mySqlBuilder.generatePlaceholders(pids));
2417        }
2418
2419        private Condition createReverseSearchPredicateLastUpdated(
2420                        List<List<IQueryParameterType>> theAndOrParams, DbColumn theSourceColumn) {
2421
2422                ResourceTablePredicateBuilder resourceTableJoin =
2423                                mySqlBuilder.addResourceTablePredicateBuilder(theSourceColumn);
2424
2425                List<Condition> andPredicates = new ArrayList<>(theAndOrParams.size());
2426
2427                for (List<IQueryParameterType> aList : theAndOrParams) {
2428                        if (!aList.isEmpty()) {
2429                                DateParam dateParam = (DateParam) aList.get(0);
2430                                DateRangeParam dateRangeParam = new DateRangeParam(dateParam);
2431                                Condition aCondition = mySqlBuilder.addPredicateLastUpdated(dateRangeParam, resourceTableJoin);
2432                                andPredicates.add(aCondition);
2433                        }
2434                }
2435
2436                return toAndPredicate(andPredicates);
2437        }
2438
2439        @Nullable
2440        private Condition createPredicateSearchParameter(
2441                        @Nullable DbColumn theSourceJoinColumn,
2442                        String theResourceName,
2443                        String theParamName,
2444                        List<List<IQueryParameterType>> theAndOrParams,
2445                        RequestDetails theRequest,
2446                        RequestPartitionId theRequestPartitionId) {
2447                List<Condition> andPredicates = new ArrayList<>();
2448                RuntimeSearchParam nextParamDef = mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName);
2449                if (nextParamDef != null) {
2450
2451                        if (myPartitionSettings.isPartitioningEnabled() && myPartitionSettings.isIncludePartitionInSearchHashes()) {
2452                                if (theRequestPartitionId.isAllPartitions()) {
2453                                        throw new PreconditionFailedException(
2454                                                        Msg.code(1220) + "This server is not configured to support search against all partitions");
2455                                }
2456                        }
2457
2458                        switch (nextParamDef.getParamType()) {
2459                                case DATE:
2460                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
2461                                                // FT: 2021-01-18 use operation 'gt', 'ge', 'le' or 'lt'
2462                                                // to create the predicateDate instead of generic one with operation = null
2463                                                SearchFilterParser.CompareOperation operation = null;
2464                                                if (nextAnd.size() > 0) {
2465                                                        DateParam param = (DateParam) nextAnd.get(0);
2466                                                        operation = toOperation(param.getPrefix());
2467                                                }
2468                                                andPredicates.add(createPredicateDate(
2469                                                                theSourceJoinColumn,
2470                                                                theResourceName,
2471                                                                null,
2472                                                                nextParamDef,
2473                                                                nextAnd,
2474                                                                operation,
2475                                                                theRequestPartitionId));
2476                                        }
2477                                        break;
2478                                case QUANTITY:
2479                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
2480                                                SearchFilterParser.CompareOperation operation = null;
2481                                                if (nextAnd.size() > 0) {
2482                                                        QuantityParam param = (QuantityParam) nextAnd.get(0);
2483                                                        operation = toOperation(param.getPrefix());
2484                                                }
2485                                                andPredicates.add(createPredicateQuantity(
2486                                                                theSourceJoinColumn,
2487                                                                theResourceName,
2488                                                                null,
2489                                                                nextParamDef,
2490                                                                nextAnd,
2491                                                                operation,
2492                                                                theRequestPartitionId));
2493                                        }
2494                                        break;
2495                                case REFERENCE:
2496                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
2497
2498                                                // Handle Search Parameters where the name is a full chain
2499                                                // (e.g. SearchParameter with name=composition.patient.identifier)
2500                                                if (handleFullyChainedParameter(
2501                                                                theSourceJoinColumn,
2502                                                                theResourceName,
2503                                                                theParamName,
2504                                                                theRequest,
2505                                                                theRequestPartitionId,
2506                                                                andPredicates,
2507                                                                nextAnd)) {
2508                                                        continue;
2509                                                }
2510
2511                                                EmbeddedChainedSearchModeEnum embeddedChainedSearchModeEnum =
2512                                                                isEligibleForEmbeddedChainedResourceSearch(theResourceName, theParamName, nextAnd);
2513                                                if (embeddedChainedSearchModeEnum == EmbeddedChainedSearchModeEnum.REF_JOIN_ONLY) {
2514                                                        andPredicates.add(createPredicateReference(
2515                                                                        theSourceJoinColumn,
2516                                                                        theResourceName,
2517                                                                        theParamName,
2518                                                                        new ArrayList<>(),
2519                                                                        nextAnd,
2520                                                                        null,
2521                                                                        theRequest,
2522                                                                        theRequestPartitionId));
2523                                                } else {
2524                                                        andPredicates.add(createPredicateReferenceForEmbeddedChainedSearchResource(
2525                                                                        theSourceJoinColumn,
2526                                                                        theResourceName,
2527                                                                        nextParamDef,
2528                                                                        nextAnd,
2529                                                                        null,
2530                                                                        theRequest,
2531                                                                        theRequestPartitionId,
2532                                                                        embeddedChainedSearchModeEnum));
2533                                                }
2534                                        }
2535                                        break;
2536                                case STRING:
2537                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
2538                                                andPredicates.add(createPredicateString(
2539                                                                theSourceJoinColumn,
2540                                                                theResourceName,
2541                                                                null,
2542                                                                nextParamDef,
2543                                                                nextAnd,
2544                                                                SearchFilterParser.CompareOperation.sw,
2545                                                                theRequestPartitionId));
2546                                        }
2547                                        break;
2548                                case TOKEN:
2549                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
2550                                                if (LOCATION_POSITION.equals(nextParamDef.getPath())) {
2551                                                        andPredicates.add(createPredicateCoords(
2552                                                                        theSourceJoinColumn,
2553                                                                        theResourceName,
2554                                                                        null,
2555                                                                        nextParamDef,
2556                                                                        nextAnd,
2557                                                                        theRequestPartitionId,
2558                                                                        mySqlBuilder));
2559                                                } else {
2560                                                        andPredicates.add(createPredicateToken(
2561                                                                        theSourceJoinColumn,
2562                                                                        theResourceName,
2563                                                                        null,
2564                                                                        nextParamDef,
2565                                                                        nextAnd,
2566                                                                        null,
2567                                                                        theRequestPartitionId));
2568                                                }
2569                                        }
2570                                        break;
2571                                case NUMBER:
2572                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
2573                                                andPredicates.add(createPredicateNumber(
2574                                                                theSourceJoinColumn,
2575                                                                theResourceName,
2576                                                                null,
2577                                                                nextParamDef,
2578                                                                nextAnd,
2579                                                                null,
2580                                                                theRequestPartitionId));
2581                                        }
2582                                        break;
2583                                case COMPOSITE:
2584                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
2585                                                andPredicates.add(createPredicateComposite(
2586                                                                theSourceJoinColumn,
2587                                                                theResourceName,
2588                                                                null,
2589                                                                nextParamDef,
2590                                                                nextAnd,
2591                                                                theRequestPartitionId));
2592                                        }
2593                                        break;
2594                                case URI:
2595                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
2596                                                andPredicates.add(createPredicateUri(
2597                                                                theSourceJoinColumn,
2598                                                                theResourceName,
2599                                                                null,
2600                                                                nextParamDef,
2601                                                                nextAnd,
2602                                                                SearchFilterParser.CompareOperation.eq,
2603                                                                theRequest,
2604                                                                theRequestPartitionId));
2605                                        }
2606                                        break;
2607                                case HAS:
2608                                case SPECIAL:
2609                                        for (List<? extends IQueryParameterType> nextAnd : theAndOrParams) {
2610                                                if (LOCATION_POSITION.equals(nextParamDef.getPath())) {
2611                                                        andPredicates.add(createPredicateCoords(
2612                                                                        theSourceJoinColumn,
2613                                                                        theResourceName,
2614                                                                        null,
2615                                                                        nextParamDef,
2616                                                                        nextAnd,
2617                                                                        theRequestPartitionId,
2618                                                                        mySqlBuilder));
2619                                                }
2620                                        }
2621                                        break;
2622                        }
2623                } else {
2624                        // These are handled later
2625                        if (!Constants.PARAM_CONTENT.equals(theParamName) && !Constants.PARAM_TEXT.equals(theParamName)) {
2626                                if (Constants.PARAM_FILTER.equals(theParamName)) {
2627
2628                                        // Parse the predicates enumerated in the _filter separated by AND or OR...
2629                                        if (theAndOrParams.get(0).get(0) instanceof StringParam) {
2630                                                String filterString =
2631                                                                ((StringParam) theAndOrParams.get(0).get(0)).getValue();
2632                                                SearchFilterParser.BaseFilter filter;
2633                                                try {
2634                                                        filter = SearchFilterParser.parse(filterString);
2635                                                } catch (SearchFilterParser.FilterSyntaxException theE) {
2636                                                        throw new InvalidRequestException(
2637                                                                        Msg.code(1221) + "Error parsing _filter syntax: " + theE.getMessage());
2638                                                }
2639                                                if (filter != null) {
2640
2641                                                        if (!myStorageSettings.isFilterParameterEnabled()) {
2642                                                                throw new InvalidRequestException(Msg.code(1222) + Constants.PARAM_FILTER
2643                                                                                + " parameter is disabled on this server");
2644                                                        }
2645
2646                                                        Condition predicate = createPredicateFilter(
2647                                                                        this, filter, theResourceName, theRequest, theRequestPartitionId);
2648                                                        if (predicate != null) {
2649                                                                mySqlBuilder.addPredicate(predicate);
2650                                                        }
2651                                                }
2652                                        }
2653
2654                                } else {
2655                                        String msg = myFhirContext
2656                                                        .getLocalizer()
2657                                                        .getMessageSanitized(
2658                                                                        BaseStorageDao.class,
2659                                                                        "invalidSearchParameter",
2660                                                                        theParamName,
2661                                                                        theResourceName,
2662                                                                        mySearchParamRegistry.getValidSearchParameterNamesIncludingMeta(theResourceName));
2663                                        throw new InvalidRequestException(Msg.code(1223) + msg);
2664                                }
2665                        }
2666                }
2667
2668                return toAndPredicate(andPredicates);
2669        }
2670
2671        /**
2672         * This method handles the case of Search Parameters where the name/code
2673         * in the SP is a full chain expression. Normally to handle an expression
2674         * like <code>Observation?subject.name=foo</code> are handled by a SP
2675         * with a type of REFERENCE where the name is "subject". That is not
2676         * handled here. On the other hand, if the SP has a name value containing
2677         * the full chain (e.g. "subject.name") we handle that here.
2678         *
2679         * @return Returns {@literal true} if the search parameter was handled
2680         * by this method
2681         */
2682        private boolean handleFullyChainedParameter(
2683                        @Nullable DbColumn theSourceJoinColumn,
2684                        String theResourceName,
2685                        String theParamName,
2686                        RequestDetails theRequest,
2687                        RequestPartitionId theRequestPartitionId,
2688                        List<Condition> andPredicates,
2689                        List<? extends IQueryParameterType> nextAnd) {
2690                if (!nextAnd.isEmpty() && nextAnd.get(0) instanceof ReferenceParam) {
2691                        ReferenceParam param = (ReferenceParam) nextAnd.get(0);
2692                        if (isNotBlank(param.getChain())) {
2693                                String fullName = theParamName + "." + param.getChain();
2694                                RuntimeSearchParam fullChainParam =
2695                                                mySearchParamRegistry.getActiveSearchParam(theResourceName, fullName);
2696                                if (fullChainParam != null) {
2697                                        List<IQueryParameterType> swappedParamTypes = nextAnd.stream()
2698                                                        .map(t -> newParameterInstance(fullChainParam, null, t.getValueAsQueryToken(myFhirContext)))
2699                                                        .collect(Collectors.toList());
2700                                        List<List<IQueryParameterType>> params = List.of(swappedParamTypes);
2701                                        Condition predicate = createPredicateSearchParameter(
2702                                                        theSourceJoinColumn, theResourceName, fullName, params, theRequest, theRequestPartitionId);
2703                                        andPredicates.add(predicate);
2704                                        return true;
2705                                }
2706                        }
2707                }
2708                return false;
2709        }
2710
2711        /**
2712         * When searching using a chained search expression (e.g. "Patient?organization.name=foo")
2713         * we have a few options:
2714         * <ul>
2715         * <li>
2716         *    A. If we want to match only {@link ca.uhn.fhir.jpa.model.entity.ResourceLink} for
2717         *    paramName="organization" with a join on {@link ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString}
2718         *    with paramName="name", that's {@link EmbeddedChainedSearchModeEnum#REF_JOIN_ONLY}
2719         *    which is the standard searching case. Let's guess that 99.9% of all searches work
2720         *    this way.
2721         * </ul>
2722         * <li>
2723         *    B. If we want to match only {@link ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString}
2724         *    with paramName="organization.name", that's {@link EmbeddedChainedSearchModeEnum#UPLIFTED_ONLY}.
2725         *    We only do this if there is an uplifted refchain declared on the "organization"
2726         *    search parameter for the "name" search parameter, and contained indexing is disabled.
2727         *    This kind of index can come from indexing normal references where the search parameter
2728         *      has an uplifted refchain declared, and it can also come from indexing contained resources.
2729         *      For both of these cases, the actual index in the database is identical. But the important
2730         *    difference is that when you're searching for contained resources you also want to
2731         *    search for normal references. When you're searching for explicit refchains, no normal
2732         *    indexes matter because they'd be a duplicate of the uplifted refchain.
2733         * </li>
2734         * <li>
2735         *    C. We can also do both and return a union of the two, using
2736         *    {@link EmbeddedChainedSearchModeEnum#UPLIFTED_AND_REF_JOIN}. We do that if contained
2737         *    resource indexing is enabled since we have to assume there may be indexes
2738         *    on "organization" for both contained and non-contained Organization.
2739         *    resources.
2740         * </li>
2741         */
2742        private EmbeddedChainedSearchModeEnum isEligibleForEmbeddedChainedResourceSearch(
2743                        String theResourceType, String theParameterName, List<? extends IQueryParameterType> theParameter) {
2744                boolean indexOnContainedResources = myStorageSettings.isIndexOnContainedResources();
2745                boolean indexOnUpliftedRefchains = myStorageSettings.isIndexOnUpliftedRefchains();
2746
2747                if (!indexOnContainedResources && !indexOnUpliftedRefchains) {
2748                        return EmbeddedChainedSearchModeEnum.REF_JOIN_ONLY;
2749                }
2750
2751                boolean haveUpliftCandidates = theParameter.stream()
2752                                .filter(t -> t instanceof ReferenceParam)
2753                                .map(t -> ((ReferenceParam) t).getChain())
2754                                .filter(StringUtils::isNotBlank)
2755                                // Chains on _has can't be indexed for contained searches - At least not yet. It's not clear to me if we
2756                                // ever want to support this, it would be really hard to do.
2757                                .filter(t -> !t.startsWith(PARAM_HAS + ":"))
2758                                .anyMatch(t -> {
2759                                        if (indexOnContainedResources) {
2760                                                return true;
2761                                        }
2762                                        RuntimeSearchParam param =
2763                                                        mySearchParamRegistry.getActiveSearchParam(theResourceType, theParameterName);
2764                                        return param != null && param.hasUpliftRefchain(t);
2765                                });
2766
2767                if (haveUpliftCandidates) {
2768                        if (indexOnContainedResources) {
2769                                return EmbeddedChainedSearchModeEnum.UPLIFTED_AND_REF_JOIN;
2770                        }
2771                        return EmbeddedChainedSearchModeEnum.UPLIFTED_ONLY;
2772                } else {
2773                        return EmbeddedChainedSearchModeEnum.REF_JOIN_ONLY;
2774                }
2775        }
2776
2777        public void addPredicateCompositeUnique(String theIndexString, RequestPartitionId theRequestPartitionId) {
2778                ComboUniqueSearchParameterPredicateBuilder predicateBuilder = mySqlBuilder.addComboUniquePredicateBuilder();
2779                Condition predicate = predicateBuilder.createPredicateIndexString(theRequestPartitionId, theIndexString);
2780                mySqlBuilder.addPredicate(predicate);
2781        }
2782
2783        public void addPredicateCompositeNonUnique(String theIndexString, RequestPartitionId theRequestPartitionId) {
2784                ComboNonUniqueSearchParameterPredicateBuilder predicateBuilder =
2785                                mySqlBuilder.addComboNonUniquePredicateBuilder();
2786                Condition predicate = predicateBuilder.createPredicateHashComplete(theRequestPartitionId, theIndexString);
2787                mySqlBuilder.addPredicate(predicate);
2788        }
2789
2790        // expand out the pids
2791        public void addPredicateEverythingOperation(
2792                        String theResourceName, List<String> theTypeSourceResourceNames, Long... theTargetPids) {
2793                ResourceLinkPredicateBuilder table = mySqlBuilder.addReferencePredicateBuilder(this, null);
2794                Condition predicate =
2795                                table.createEverythingPredicate(theResourceName, theTypeSourceResourceNames, theTargetPids);
2796                mySqlBuilder.addPredicate(predicate);
2797                mySqlBuilder.getSelect().setIsDistinct(true);
2798        }
2799
2800        public IQueryParameterType newParameterInstance(
2801                        RuntimeSearchParam theParam, String theQualifier, String theValueAsQueryToken) {
2802                IQueryParameterType qp = newParameterInstance(theParam);
2803
2804                qp.setValueAsQueryToken(myFhirContext, theParam.getName(), theQualifier, theValueAsQueryToken);
2805                return qp;
2806        }
2807
2808        private IQueryParameterType newParameterInstance(RuntimeSearchParam theParam) {
2809
2810                IQueryParameterType qp;
2811                switch (theParam.getParamType()) {
2812                        case DATE:
2813                                qp = new DateParam();
2814                                break;
2815                        case NUMBER:
2816                                qp = new NumberParam();
2817                                break;
2818                        case QUANTITY:
2819                                qp = new QuantityParam();
2820                                break;
2821                        case STRING:
2822                                qp = new StringParam();
2823                                break;
2824                        case TOKEN:
2825                                qp = new TokenParam();
2826                                break;
2827                        case COMPOSITE:
2828                                List<RuntimeSearchParam> compositeOf =
2829                                                JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, theParam);
2830                                if (compositeOf.size() != 2) {
2831                                        throw new InternalErrorException(Msg.code(1224) + "Parameter " + theParam.getName() + " has "
2832                                                        + compositeOf.size() + " composite parts. Don't know how handlt this.");
2833                                }
2834                                IQueryParameterType leftParam = newParameterInstance(compositeOf.get(0));
2835                                IQueryParameterType rightParam = newParameterInstance(compositeOf.get(1));
2836                                qp = new CompositeParam<>(leftParam, rightParam);
2837                                break;
2838                        case URI:
2839                                qp = new UriParam();
2840                                break;
2841                        case REFERENCE:
2842                                qp = new ReferenceParam();
2843                                break;
2844                        case SPECIAL:
2845                                qp = new SpecialParam();
2846                                break;
2847                        case HAS:
2848                        default:
2849                                throw new InvalidRequestException(
2850                                                Msg.code(1225) + "The search type: " + theParam.getParamType() + " is not supported.");
2851                }
2852                return qp;
2853        }
2854
2855        /**
2856         * @see #isEligibleForEmbeddedChainedResourceSearch(String, String, List) for an explanation of the values in this enum
2857         */
2858        enum EmbeddedChainedSearchModeEnum {
2859                UPLIFTED_ONLY(true),
2860                UPLIFTED_AND_REF_JOIN(true),
2861                REF_JOIN_ONLY(false);
2862
2863                private final boolean mySupportsUplifted;
2864
2865                EmbeddedChainedSearchModeEnum(boolean theSupportsUplifted) {
2866                        mySupportsUplifted = theSupportsUplifted;
2867                }
2868
2869                public boolean supportsUplifted() {
2870                        return mySupportsUplifted;
2871                }
2872        }
2873
2874        private static final class ChainElement {
2875                private final String myResourceType;
2876                private final String mySearchParameterName;
2877                private final String myPath;
2878
2879                public ChainElement(String theResourceType, String theSearchParameterName, String thePath) {
2880                        this.myResourceType = theResourceType;
2881                        this.mySearchParameterName = theSearchParameterName;
2882                        this.myPath = thePath;
2883                }
2884
2885                public String getResourceType() {
2886                        return myResourceType;
2887                }
2888
2889                public String getPath() {
2890                        return myPath;
2891                }
2892
2893                public String getSearchParameterName() {
2894                        return mySearchParameterName;
2895                }
2896
2897                @Override
2898                public boolean equals(Object o) {
2899                        if (this == o) return true;
2900                        if (o == null || getClass() != o.getClass()) return false;
2901                        ChainElement that = (ChainElement) o;
2902                        return myResourceType.equals(that.myResourceType)
2903                                        && mySearchParameterName.equals(that.mySearchParameterName)
2904                                        && myPath.equals(that.myPath);
2905                }
2906
2907                @Override
2908                public int hashCode() {
2909                        return Objects.hash(myResourceType, mySearchParameterName, myPath);
2910                }
2911        }
2912
2913        private class ReferenceChainExtractor {
2914                private final Map<List<ChainElement>, Set<LeafNodeDefinition>> myChains = Maps.newHashMap();
2915
2916                public Map<List<ChainElement>, Set<LeafNodeDefinition>> getChains() {
2917                        return myChains;
2918                }
2919
2920                private boolean isReferenceParamValid(ReferenceParam theReferenceParam) {
2921                        return split(theReferenceParam.getChain(), '.').length <= 3;
2922                }
2923
2924                private List<String> extractPaths(String theResourceType, RuntimeSearchParam theSearchParam) {
2925                        List<String> pathsForType = theSearchParam.getPathsSplit().stream()
2926                                        .map(String::trim)
2927                                        .filter(t -> (t.startsWith(theResourceType) || t.startsWith("(" + theResourceType)))
2928                                        .collect(Collectors.toList());
2929                        if (pathsForType.isEmpty()) {
2930                                ourLog.warn(
2931                                                "Search parameter {} does not have a path for resource type {}.",
2932                                                theSearchParam.getName(),
2933                                                theResourceType);
2934                        }
2935
2936                        return pathsForType;
2937                }
2938
2939                public void deriveChains(
2940                                String theResourceType,
2941                                RuntimeSearchParam theSearchParam,
2942                                List<? extends IQueryParameterType> theList) {
2943                        List<String> paths = extractPaths(theResourceType, theSearchParam);
2944                        for (String path : paths) {
2945                                List<ChainElement> searchParams = Lists.newArrayList();
2946                                searchParams.add(new ChainElement(theResourceType, theSearchParam.getName(), path));
2947                                for (IQueryParameterType nextOr : theList) {
2948                                        String targetValue = nextOr.getValueAsQueryToken(myFhirContext);
2949                                        if (nextOr instanceof ReferenceParam) {
2950                                                ReferenceParam referenceParam = (ReferenceParam) nextOr;
2951                                                if (!isReferenceParamValid(referenceParam)) {
2952                                                        throw new InvalidRequestException(Msg.code(2007) + "The search chain "
2953                                                                        + theSearchParam.getName() + "." + referenceParam.getChain()
2954                                                                        + " is too long. Only chains up to three references are supported.");
2955                                                }
2956
2957                                                String targetChain = referenceParam.getChain();
2958                                                List<String> qualifiers = Lists.newArrayList(referenceParam.getResourceType());
2959
2960                                                processNextLinkInChain(
2961                                                                searchParams,
2962                                                                theSearchParam,
2963                                                                targetChain,
2964                                                                targetValue,
2965                                                                qualifiers,
2966                                                                referenceParam.getResourceType());
2967                                        }
2968                                }
2969                        }
2970                }
2971
2972                private void processNextLinkInChain(
2973                                List<ChainElement> theSearchParams,
2974                                RuntimeSearchParam thePreviousSearchParam,
2975                                String theChain,
2976                                String theTargetValue,
2977                                List<String> theQualifiers,
2978                                String theResourceType) {
2979
2980                        String nextParamName = theChain;
2981                        String nextChain = null;
2982                        String nextQualifier = null;
2983                        int linkIndex = theChain.indexOf('.');
2984                        if (linkIndex != -1) {
2985                                nextParamName = theChain.substring(0, linkIndex);
2986                                nextChain = theChain.substring(linkIndex + 1);
2987                        }
2988
2989                        int qualifierIndex = nextParamName.indexOf(':');
2990                        if (qualifierIndex != -1) {
2991                                nextParamName = nextParamName.substring(0, qualifierIndex);
2992                                nextQualifier = nextParamName.substring(qualifierIndex);
2993                        }
2994
2995                        List<String> qualifiersBranch = Lists.newArrayList();
2996                        qualifiersBranch.addAll(theQualifiers);
2997                        qualifiersBranch.add(nextQualifier);
2998
2999                        boolean searchParamFound = false;
3000                        for (String nextTarget : thePreviousSearchParam.getTargets()) {
3001                                RuntimeSearchParam nextSearchParam = null;
3002                                if (isBlank(theResourceType) || theResourceType.equals(nextTarget)) {
3003                                        nextSearchParam = mySearchParamRegistry.getActiveSearchParam(nextTarget, nextParamName);
3004                                }
3005                                if (nextSearchParam != null) {
3006                                        searchParamFound = true;
3007                                        // If we find a search param on this resource type for this parameter name, keep iterating
3008                                        //  Otherwise, abandon this branch and carry on to the next one
3009                                        if (StringUtils.isEmpty(nextChain)) {
3010                                                // We've reached the end of the chain
3011                                                ArrayList<IQueryParameterType> orValues = Lists.newArrayList();
3012
3013                                                if (RestSearchParameterTypeEnum.REFERENCE.equals(nextSearchParam.getParamType())) {
3014                                                        orValues.add(new ReferenceParam(nextQualifier, "", theTargetValue));
3015                                                } else {
3016                                                        IQueryParameterType qp = newParameterInstance(nextSearchParam);
3017                                                        qp.setValueAsQueryToken(myFhirContext, nextSearchParam.getName(), null, theTargetValue);
3018                                                        orValues.add(qp);
3019                                                }
3020
3021                                                Set<LeafNodeDefinition> leafNodes = myChains.get(theSearchParams);
3022                                                if (leafNodes == null) {
3023                                                        leafNodes = Sets.newHashSet();
3024                                                        myChains.put(theSearchParams, leafNodes);
3025                                                }
3026                                                leafNodes.add(new LeafNodeDefinition(
3027                                                                nextSearchParam, orValues, nextTarget, nextParamName, "", qualifiersBranch));
3028                                        } else {
3029                                                List<String> nextPaths = extractPaths(nextTarget, nextSearchParam);
3030                                                for (String nextPath : nextPaths) {
3031                                                        List<ChainElement> searchParamBranch = Lists.newArrayList();
3032                                                        searchParamBranch.addAll(theSearchParams);
3033
3034                                                        searchParamBranch.add(new ChainElement(nextTarget, nextSearchParam.getName(), nextPath));
3035                                                        processNextLinkInChain(
3036                                                                        searchParamBranch,
3037                                                                        nextSearchParam,
3038                                                                        nextChain,
3039                                                                        theTargetValue,
3040                                                                        qualifiersBranch,
3041                                                                        nextQualifier);
3042                                                }
3043                                        }
3044                                }
3045                        }
3046                        if (!searchParamFound) {
3047                                throw new InvalidRequestException(Msg.code(1214)
3048                                                + myFhirContext
3049                                                                .getLocalizer()
3050                                                                .getMessage(
3051                                                                                BaseStorageDao.class,
3052                                                                                "invalidParameterChain",
3053                                                                                thePreviousSearchParam.getName() + '.' + theChain));
3054                        }
3055                }
3056        }
3057
3058        private static class LeafNodeDefinition {
3059                private final RuntimeSearchParam myParamDefinition;
3060                private final ArrayList<IQueryParameterType> myOrValues;
3061                private final String myLeafTarget;
3062                private final String myLeafParamName;
3063                private final String myLeafPathPrefix;
3064                private final List<String> myQualifiers;
3065
3066                public LeafNodeDefinition(
3067                                RuntimeSearchParam theParamDefinition,
3068                                ArrayList<IQueryParameterType> theOrValues,
3069                                String theLeafTarget,
3070                                String theLeafParamName,
3071                                String theLeafPathPrefix,
3072                                List<String> theQualifiers) {
3073                        myParamDefinition = theParamDefinition;
3074                        myOrValues = theOrValues;
3075                        myLeafTarget = theLeafTarget;
3076                        myLeafParamName = theLeafParamName;
3077                        myLeafPathPrefix = theLeafPathPrefix;
3078                        myQualifiers = theQualifiers;
3079                }
3080
3081                public RuntimeSearchParam getParamDefinition() {
3082                        return myParamDefinition;
3083                }
3084
3085                public ArrayList<IQueryParameterType> getOrValues() {
3086                        return myOrValues;
3087                }
3088
3089                public String getLeafTarget() {
3090                        return myLeafTarget;
3091                }
3092
3093                public String getLeafParamName() {
3094                        return myLeafParamName;
3095                }
3096
3097                public String getLeafPathPrefix() {
3098                        return myLeafPathPrefix;
3099                }
3100
3101                public List<String> getQualifiers() {
3102                        return myQualifiers;
3103                }
3104
3105                public LeafNodeDefinition withPathPrefix(String theResourceType, String theName) {
3106                        return new LeafNodeDefinition(
3107                                        myParamDefinition, myOrValues, theResourceType, myLeafParamName, theName, myQualifiers);
3108                }
3109
3110                @Override
3111                public boolean equals(Object o) {
3112                        if (this == o) return true;
3113                        if (o == null || getClass() != o.getClass()) return false;
3114                        LeafNodeDefinition that = (LeafNodeDefinition) o;
3115                        return Objects.equals(myParamDefinition, that.myParamDefinition)
3116                                        && Objects.equals(myOrValues, that.myOrValues)
3117                                        && Objects.equals(myLeafTarget, that.myLeafTarget)
3118                                        && Objects.equals(myLeafParamName, that.myLeafParamName)
3119                                        && Objects.equals(myLeafPathPrefix, that.myLeafPathPrefix)
3120                                        && Objects.equals(myQualifiers, that.myQualifiers);
3121                }
3122
3123                @Override
3124                public int hashCode() {
3125                        return Objects.hash(
3126                                        myParamDefinition, myOrValues, myLeafTarget, myLeafParamName, myLeafPathPrefix, myQualifiers);
3127                }
3128
3129                /**
3130                 * Return a copy of this object with the given {@link RuntimeSearchParam}
3131                 * but all other values unchanged.
3132                 */
3133                public LeafNodeDefinition withParam(RuntimeSearchParam theParamDefinition) {
3134                        return new LeafNodeDefinition(
3135                                        theParamDefinition, myOrValues, myLeafTarget, myLeafParamName, myLeafPathPrefix, myQualifiers);
3136                }
3137        }
3138
3139        public static class SearchForIdsParams {
3140                DbColumn mySourceJoinColumn;
3141                String myResourceName;
3142                String myParamName;
3143                List<List<IQueryParameterType>> myAndOrParams;
3144                RequestDetails myRequest;
3145                RequestPartitionId myRequestPartitionId;
3146                ResourceTablePredicateBuilder myResourceTablePredicateBuilder;
3147
3148                public static SearchForIdsParams with() {
3149                        return new SearchForIdsParams();
3150                }
3151
3152                public DbColumn getSourceJoinColumn() {
3153                        return mySourceJoinColumn;
3154                }
3155
3156                public SearchForIdsParams setSourceJoinColumn(DbColumn theSourceJoinColumn) {
3157                        mySourceJoinColumn = theSourceJoinColumn;
3158                        return this;
3159                }
3160
3161                public String getResourceName() {
3162                        return myResourceName;
3163                }
3164
3165                public SearchForIdsParams setResourceName(String theResourceName) {
3166                        myResourceName = theResourceName;
3167                        return this;
3168                }
3169
3170                public String getParamName() {
3171                        return myParamName;
3172                }
3173
3174                public SearchForIdsParams setParamName(String theParamName) {
3175                        myParamName = theParamName;
3176                        return this;
3177                }
3178
3179                public List<List<IQueryParameterType>> getAndOrParams() {
3180                        return myAndOrParams;
3181                }
3182
3183                public SearchForIdsParams setAndOrParams(List<List<IQueryParameterType>> theAndOrParams) {
3184                        myAndOrParams = theAndOrParams;
3185                        return this;
3186                }
3187
3188                public RequestDetails getRequest() {
3189                        return myRequest;
3190                }
3191
3192                public SearchForIdsParams setRequest(RequestDetails theRequest) {
3193                        myRequest = theRequest;
3194                        return this;
3195                }
3196
3197                public RequestPartitionId getRequestPartitionId() {
3198                        return myRequestPartitionId;
3199                }
3200
3201                public SearchForIdsParams setRequestPartitionId(RequestPartitionId theRequestPartitionId) {
3202                        myRequestPartitionId = theRequestPartitionId;
3203                        return this;
3204                }
3205
3206                public ResourceTablePredicateBuilder getResourceTablePredicateBuilder() {
3207                        return myResourceTablePredicateBuilder;
3208                }
3209
3210                public SearchForIdsParams setResourceTablePredicateBuilder(
3211                                ResourceTablePredicateBuilder theResourceTablePredicateBuilder) {
3212                        myResourceTablePredicateBuilder = theResourceTablePredicateBuilder;
3213                        return this;
3214                }
3215        }
3216}