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