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