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