001/*-
002 * #%L
003 * HAPI FHIR JPA Server
004 * %%
005 * Copyright (C) 2014 - 2023 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.sql;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.i18n.Msg;
024import ca.uhn.fhir.interceptor.model.RequestPartitionId;
025import ca.uhn.fhir.jpa.config.HibernatePropertiesProvider;
026import ca.uhn.fhir.jpa.model.config.PartitionSettings;
027import ca.uhn.fhir.jpa.model.dao.JpaPid;
028import ca.uhn.fhir.jpa.model.entity.StorageSettings;
029import ca.uhn.fhir.jpa.search.builder.QueryStack;
030import ca.uhn.fhir.jpa.search.builder.predicate.BaseJoiningPredicateBuilder;
031import ca.uhn.fhir.jpa.search.builder.predicate.ComboNonUniqueSearchParameterPredicateBuilder;
032import ca.uhn.fhir.jpa.search.builder.predicate.ComboUniqueSearchParameterPredicateBuilder;
033import ca.uhn.fhir.jpa.search.builder.predicate.CoordsPredicateBuilder;
034import ca.uhn.fhir.jpa.search.builder.predicate.DatePredicateBuilder;
035import ca.uhn.fhir.jpa.search.builder.predicate.ForcedIdPredicateBuilder;
036import ca.uhn.fhir.jpa.search.builder.predicate.NumberPredicateBuilder;
037import ca.uhn.fhir.jpa.search.builder.predicate.QuantityNormalizedPredicateBuilder;
038import ca.uhn.fhir.jpa.search.builder.predicate.QuantityPredicateBuilder;
039import ca.uhn.fhir.jpa.search.builder.predicate.ResourceIdPredicateBuilder;
040import ca.uhn.fhir.jpa.search.builder.predicate.ResourceLinkPredicateBuilder;
041import ca.uhn.fhir.jpa.search.builder.predicate.ResourceTablePredicateBuilder;
042import ca.uhn.fhir.jpa.search.builder.predicate.SearchParamPresentPredicateBuilder;
043import ca.uhn.fhir.jpa.search.builder.predicate.SourcePredicateBuilder;
044import ca.uhn.fhir.jpa.search.builder.predicate.StringPredicateBuilder;
045import ca.uhn.fhir.jpa.search.builder.predicate.TagPredicateBuilder;
046import ca.uhn.fhir.jpa.search.builder.predicate.TokenPredicateBuilder;
047import ca.uhn.fhir.jpa.search.builder.predicate.UriPredicateBuilder;
048import ca.uhn.fhir.rest.param.DateParam;
049import ca.uhn.fhir.rest.param.DateRangeParam;
050import ca.uhn.fhir.rest.param.ParamPrefixEnum;
051import com.healthmarketscience.sqlbuilder.BinaryCondition;
052import com.healthmarketscience.sqlbuilder.ComboCondition;
053import com.healthmarketscience.sqlbuilder.ComboExpression;
054import com.healthmarketscience.sqlbuilder.Condition;
055import com.healthmarketscience.sqlbuilder.FunctionCall;
056import com.healthmarketscience.sqlbuilder.InCondition;
057import com.healthmarketscience.sqlbuilder.OrderObject;
058import com.healthmarketscience.sqlbuilder.SelectQuery;
059import com.healthmarketscience.sqlbuilder.dbspec.Join;
060import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
061import com.healthmarketscience.sqlbuilder.dbspec.basic.DbJoin;
062import com.healthmarketscience.sqlbuilder.dbspec.basic.DbSchema;
063import com.healthmarketscience.sqlbuilder.dbspec.basic.DbSpec;
064import com.healthmarketscience.sqlbuilder.dbspec.basic.DbTable;
065import org.apache.commons.lang3.Validate;
066import org.hibernate.dialect.Dialect;
067import org.hibernate.dialect.SQLServerDialect;
068import org.hibernate.dialect.pagination.AbstractLimitHandler;
069import org.hibernate.engine.spi.RowSelection;
070import org.slf4j.Logger;
071import org.slf4j.LoggerFactory;
072
073import javax.annotation.Nonnull;
074import javax.annotation.Nullable;
075import java.util.ArrayList;
076import java.util.Collection;
077import java.util.List;
078import java.util.Set;
079import java.util.UUID;
080import java.util.stream.Collectors;
081
082import static ca.uhn.fhir.rest.param.ParamPrefixEnum.GREATERTHAN;
083import static ca.uhn.fhir.rest.param.ParamPrefixEnum.GREATERTHAN_OR_EQUALS;
084import static ca.uhn.fhir.rest.param.ParamPrefixEnum.LESSTHAN;
085import static ca.uhn.fhir.rest.param.ParamPrefixEnum.LESSTHAN_OR_EQUALS;
086import static ca.uhn.fhir.rest.param.ParamPrefixEnum.NOT_EQUAL;
087import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
088
089public class SearchQueryBuilder {
090
091        private static final Logger ourLog = LoggerFactory.getLogger(SearchQueryBuilder.class);
092        private final String myBindVariableSubstitutionBase;
093        private final ArrayList<Object> myBindVariableValues;
094        private final DbSpec mySpec;
095        private final DbSchema mySchema;
096        private final SelectQuery mySelect;
097        private final PartitionSettings myPartitionSettings;
098        private final RequestPartitionId myRequestPartitionId;
099        private final String myResourceType;
100        private final StorageSettings myStorageSettings;
101        private final FhirContext myFhirContext;
102        private final SqlObjectFactory mySqlBuilderFactory;
103        private final boolean myCountQuery;
104        private final Dialect myDialect;
105        private boolean myMatchNothing;
106        private ResourceTablePredicateBuilder myResourceTableRoot;
107        private boolean myHaveAtLeastOnePredicate;
108        private BaseJoiningPredicateBuilder myFirstPredicateBuilder;
109        private boolean dialectIsMsSql;
110        private boolean dialectIsMySql;
111        private boolean myNeedResourceTableRoot;
112        private int myNextNearnessColumnId = 0;
113
114        /**
115         * Constructor
116         */
117        public SearchQueryBuilder(FhirContext theFhirContext, StorageSettings theStorageSettings, PartitionSettings thePartitionSettings, RequestPartitionId theRequestPartitionId, String theResourceType, SqlObjectFactory theSqlBuilderFactory, HibernatePropertiesProvider theDialectProvider, boolean theCountQuery) {
118                this(theFhirContext, theStorageSettings, thePartitionSettings, theRequestPartitionId, theResourceType, theSqlBuilderFactory, UUID.randomUUID() + "-", theDialectProvider.getDialect(), theCountQuery, new ArrayList<>());
119        }
120
121        /**
122         * Constructor for child SQL Builders
123         */
124        private SearchQueryBuilder(FhirContext theFhirContext, StorageSettings theStorageSettings, PartitionSettings thePartitionSettings, RequestPartitionId theRequestPartitionId, String theResourceType, SqlObjectFactory theSqlBuilderFactory, String theBindVariableSubstitutionBase, Dialect theDialect, boolean theCountQuery, ArrayList<Object> theBindVariableValues) {
125                myFhirContext = theFhirContext;
126                myStorageSettings = theStorageSettings;
127                myPartitionSettings = thePartitionSettings;
128                myRequestPartitionId = theRequestPartitionId;
129                myResourceType = theResourceType;
130                mySqlBuilderFactory = theSqlBuilderFactory;
131                myCountQuery = theCountQuery;
132                myDialect = theDialect;
133                if (myDialect instanceof org.hibernate.dialect.MySQLDialect) {
134                        dialectIsMySql = true;
135                }
136                if (myDialect instanceof org.hibernate.dialect.SQLServerDialect) {
137                        dialectIsMsSql = true;
138                }
139
140
141                mySpec = new DbSpec();
142                mySchema = mySpec.addDefaultSchema();
143                mySelect = new SelectQuery();
144
145                myBindVariableSubstitutionBase = theBindVariableSubstitutionBase;
146                myBindVariableValues = theBindVariableValues;
147        }
148
149        public FhirContext getFhirContext() {
150                return myFhirContext;
151        }
152
153        /**
154         * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on a Composite Unique search parameter
155         */
156        public ComboUniqueSearchParameterPredicateBuilder addComboUniquePredicateBuilder() {
157                ComboUniqueSearchParameterPredicateBuilder retVal = mySqlBuilderFactory.newComboUniqueSearchParameterPredicateBuilder(this);
158                addTable(retVal, null);
159                return retVal;
160        }
161
162        /**
163         * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on a Composite Unique search parameter
164         */
165        public ComboNonUniqueSearchParameterPredicateBuilder addComboNonUniquePredicateBuilder() {
166                ComboNonUniqueSearchParameterPredicateBuilder retVal = mySqlBuilderFactory.newComboNonUniqueSearchParameterPredicateBuilder(this);
167                addTable(retVal, null);
168                return retVal;
169        }
170
171        /**
172         * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on a COORDS search parameter
173         */
174        public CoordsPredicateBuilder addCoordsPredicateBuilder(@Nullable DbColumn theSourceJoinColumn) {
175                CoordsPredicateBuilder retVal = mySqlBuilderFactory.coordsPredicateBuilder(this);
176                addTable(retVal, theSourceJoinColumn);
177                return retVal;
178        }
179
180        /**
181         * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a DATE search parameter
182         */
183        public DatePredicateBuilder addDatePredicateBuilder(@Nullable DbColumn theSourceJoinColumn) {
184                DatePredicateBuilder retVal = mySqlBuilderFactory.dateIndexTable(this);
185                addTable(retVal, theSourceJoinColumn);
186                return retVal;
187        }
188
189        /**
190         * Create a predicate builder for selecting on a DATE search parameter
191         */
192        public DatePredicateBuilder createDatePredicateBuilder() {
193                return mySqlBuilderFactory.dateIndexTable(this);
194        }
195
196        /**
197         * Add and return a predicate builder for selecting a forced ID. This is only intended for use with sorts so it can not
198         * be the root query.
199         */
200        public ForcedIdPredicateBuilder addForcedIdPredicateBuilder(@Nonnull DbColumn theSourceJoinColumn) {
201                Validate.isTrue(theSourceJoinColumn != null);
202
203                ForcedIdPredicateBuilder retVal = mySqlBuilderFactory.newForcedIdPredicateBuilder(this);
204                addTableForSorting(retVal, theSourceJoinColumn);
205                return retVal;
206        }
207
208        /**
209         * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a NUMBER search parameter
210         */
211        public NumberPredicateBuilder addNumberPredicateBuilder(@Nullable DbColumn theSourceJoinColumn) {
212                NumberPredicateBuilder retVal = createNumberPredicateBuilder();
213                addTable(retVal, theSourceJoinColumn);
214                return retVal;
215        }
216
217        /**
218         * Create a predicate builder for selecting on a NUMBER search parameter
219         */
220        public NumberPredicateBuilder createNumberPredicateBuilder() {
221                return mySqlBuilderFactory.numberIndexTable(this);
222        }
223
224        /**
225         * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on the Resource table
226         */
227        public ResourceTablePredicateBuilder addResourceTablePredicateBuilder(@Nullable DbColumn theSourceJoinColumn) {
228                ResourceTablePredicateBuilder retVal = mySqlBuilderFactory.resourceTable(this);
229                addTable(retVal, theSourceJoinColumn);
230                return retVal;
231        }
232
233        /**
234         * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a QUANTITY search parameter
235         */
236        public QuantityPredicateBuilder addQuantityPredicateBuilder(@Nullable DbColumn theSourceJoinColumn) {
237                QuantityPredicateBuilder retVal = createQuantityPredicateBuilder();
238                addTable(retVal, theSourceJoinColumn);
239
240                return retVal;
241        }
242
243        /**
244         * Create a predicate builder for selecting on a QUANTITY search parameter
245         */
246        public QuantityPredicateBuilder createQuantityPredicateBuilder() {
247                return mySqlBuilderFactory.quantityIndexTable(this);
248        }
249
250        public QuantityNormalizedPredicateBuilder addQuantityNormalizedPredicateBuilder(@Nullable DbColumn theSourceJoinColumn) {
251
252                QuantityNormalizedPredicateBuilder retVal = mySqlBuilderFactory.quantityNormalizedIndexTable(this);
253                addTable(retVal, theSourceJoinColumn);
254
255                return retVal;
256        }
257
258        /**
259         * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on a <code>_source</code> search parameter
260         */
261        public SourcePredicateBuilder addSourcePredicateBuilder(@Nullable DbColumn theSourceJoinColumn) {
262                SourcePredicateBuilder retVal = mySqlBuilderFactory.newSourcePredicateBuilder(this);
263                addTable(retVal, theSourceJoinColumn);
264                return retVal;
265        }
266
267        /**
268         * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a REFERENCE search parameter
269         */
270        public ResourceLinkPredicateBuilder addReferencePredicateBuilder(QueryStack theQueryStack, @Nullable DbColumn theSourceJoinColumn) {
271                ResourceLinkPredicateBuilder retVal = createReferencePredicateBuilder(theQueryStack);
272                addTable(retVal, theSourceJoinColumn);
273                return retVal;
274        }
275
276        /**
277         * Create a predicate builder for selecting on a REFERENCE search parameter
278         */
279        public ResourceLinkPredicateBuilder createReferencePredicateBuilder(QueryStack theQueryStack) {
280                return mySqlBuilderFactory.referenceIndexTable(theQueryStack, this, false);
281        }
282
283        /**
284         * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on a resource link where the
285         * source and target are reversed. This is used for _has queries.
286         */
287        public ResourceLinkPredicateBuilder addReferencePredicateBuilderReversed(QueryStack theQueryStack, DbColumn theSourceJoinColumn) {
288                ResourceLinkPredicateBuilder retVal = mySqlBuilderFactory.referenceIndexTable(theQueryStack, this, true);
289                addTable(retVal, theSourceJoinColumn);
290                return retVal;
291        }
292
293        /**
294         * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a STRING search parameter
295         */
296        public StringPredicateBuilder addStringPredicateBuilder(@Nullable DbColumn theSourceJoinColumn) {
297                StringPredicateBuilder retVal = createStringPredicateBuilder();
298                addTable(retVal, theSourceJoinColumn);
299                return retVal;
300        }
301
302        /**
303         * Create a predicate builder for selecting on a STRING search parameter
304         */
305        public StringPredicateBuilder createStringPredicateBuilder() {
306                return mySqlBuilderFactory.stringIndexTable(this);
307        }
308
309        /**
310         * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on a <code>_tag</code> search parameter
311         */
312        public TagPredicateBuilder addTagPredicateBuilder(@Nullable DbColumn theSourceJoinColumn) {
313                TagPredicateBuilder retVal = mySqlBuilderFactory.newTagPredicateBuilder(this);
314                addTable(retVal, theSourceJoinColumn);
315                return retVal;
316        }
317
318        /**
319         * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a TOKEN search parameter
320         */
321        public TokenPredicateBuilder addTokenPredicateBuilder(@Nullable DbColumn theSourceJoinColumn) {
322                TokenPredicateBuilder retVal = createTokenPredicateBuilder();
323                addTable(retVal, theSourceJoinColumn);
324                return retVal;
325        }
326
327        /**
328         * Create a predicate builder for selecting on a TOKEN search parameter
329         */
330        public TokenPredicateBuilder createTokenPredicateBuilder() {
331                return mySqlBuilderFactory.tokenIndexTable(this);
332        }
333
334        public void addCustomJoin(SelectQuery.JoinType theJoinType, DbTable theFromTable, DbTable theToTable, Condition theCondition) {
335                mySelect.addCustomJoin(theJoinType, theFromTable, theToTable, theCondition);
336        }
337
338        public ComboCondition createOnCondition(DbColumn theSourceColumn, DbColumn theTargetColumn) {
339                ComboCondition onCondition = ComboCondition.and();
340                onCondition.addCondition(BinaryCondition.equalTo(theSourceColumn, theTargetColumn));
341
342                return onCondition;
343        }
344
345        /**
346         * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on a <code>:missing</code> search parameter
347         */
348        public SearchParamPresentPredicateBuilder addSearchParamPresentPredicateBuilder(@Nullable DbColumn theSourceJoinColumn) {
349                SearchParamPresentPredicateBuilder retVal = mySqlBuilderFactory.searchParamPresentPredicateBuilder(this);
350                addTable(retVal, theSourceJoinColumn);
351                return retVal;
352        }
353
354        /**
355         * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a URI search parameter
356         */
357        public UriPredicateBuilder addUriPredicateBuilder(@Nullable DbColumn theSourceJoinColumn) {
358                UriPredicateBuilder retVal = createUriPredicateBuilder();
359                addTable(retVal, theSourceJoinColumn);
360                return retVal;
361        }
362
363        /**
364         * Create a predicate builder for selecting on a URI search parameter
365         */
366        public UriPredicateBuilder createUriPredicateBuilder() {
367                return mySqlBuilderFactory.uriIndexTable(this);
368        }
369
370        public SqlObjectFactory getSqlBuilderFactory() {
371                return mySqlBuilderFactory;
372        }
373
374        public ResourceIdPredicateBuilder newResourceIdBuilder() {
375                return mySqlBuilderFactory.resourceId(this);
376        }
377
378
379        /**
380         * Add and return a predicate builder (or a root query if no root query exists yet) for an arbitrary table
381         */
382        private void addTable(BaseJoiningPredicateBuilder thePredicateBuilder, @Nullable DbColumn theSourceJoinColumn) {
383                addTable(thePredicateBuilder, theSourceJoinColumn, SelectQuery.JoinType.INNER);
384        }
385
386        private void addTableForSorting(BaseJoiningPredicateBuilder thePredicateBuilder, @Nullable DbColumn theSourceJoinColumn) {
387                addTable(thePredicateBuilder, theSourceJoinColumn, SelectQuery.JoinType.LEFT_OUTER);
388        }
389
390        private void addTable(BaseJoiningPredicateBuilder thePredicateBuilder, @Nullable DbColumn theSourceJoinColumn, SelectQuery.JoinType theJoinType) {
391                if (theSourceJoinColumn != null) {
392                        DbTable fromTable = theSourceJoinColumn.getTable();
393                        DbTable toTable = thePredicateBuilder.getTable();
394                        DbColumn toColumn = thePredicateBuilder.getResourceIdColumn();
395                        addJoin(fromTable, toTable, theSourceJoinColumn, toColumn, theJoinType);
396                } else {
397                        if (myFirstPredicateBuilder == null) {
398
399                                BaseJoiningPredicateBuilder root;
400                                if (!myNeedResourceTableRoot) {
401                                        root = thePredicateBuilder;
402                                } else {
403                                        if (thePredicateBuilder instanceof ResourceTablePredicateBuilder) {
404                                                root = thePredicateBuilder;
405                                        } else {
406                                                root = mySqlBuilderFactory.resourceTable(this);
407                                        }
408                                }
409
410                                if (myCountQuery) {
411                                        mySelect.addCustomColumns(FunctionCall.count().setIsDistinct(true).addColumnParams(root.getResourceIdColumn()));
412                                } else {
413                                        mySelect.addColumns(root.getResourceIdColumn());
414                                }
415                                mySelect.addFromTable(root.getTable());
416                                myFirstPredicateBuilder = root;
417
418                                if (!myNeedResourceTableRoot || (thePredicateBuilder instanceof ResourceTablePredicateBuilder)) {
419                                        return;
420                                }
421                        }
422
423                        DbTable fromTable = myFirstPredicateBuilder.getTable();
424                        DbTable toTable = thePredicateBuilder.getTable();
425                        DbColumn fromColumn = myFirstPredicateBuilder.getResourceIdColumn();
426                        DbColumn toColumn = thePredicateBuilder.getResourceIdColumn();
427                        addJoin(fromTable, toTable, fromColumn, toColumn, theJoinType);
428                }
429        }
430
431
432        public void addJoin(DbTable theFromTable, DbTable theToTable, DbColumn theFromColumn, DbColumn theToColumn, SelectQuery.JoinType theJoinType) {
433                Join join = new DbJoin(mySpec, theFromTable, theToTable, new DbColumn[]{theFromColumn}, new DbColumn[]{theToColumn});
434                mySelect.addJoins(theJoinType, join);
435        }
436
437        public void addJoin(DbTable theFromTable, DbTable theToTable, DbColumn theFromColumn, DbColumn theToColumn) {
438                Join join = new DbJoin(mySpec, theFromTable, theToTable, new DbColumn[]{theFromColumn}, new DbColumn[]{theToColumn});
439                mySelect.addJoins(SelectQuery.JoinType.INNER, join);
440        }
441
442        /**
443         * Generate and return the SQL generated by this builder
444         */
445        public GeneratedSql generate(@Nullable Integer theOffset, @Nullable Integer theMaxResultsToFetch) {
446
447                getOrCreateFirstPredicateBuilder();
448
449                mySelect.validate();
450                String sql = mySelect.toString();
451
452                List<Object> bindVariables = new ArrayList<>();
453                while (true) {
454
455                        int idx = sql.indexOf(myBindVariableSubstitutionBase);
456                        if (idx == -1) {
457                                break;
458                        }
459
460                        int endIdx = sql.indexOf("'", idx + myBindVariableSubstitutionBase.length());
461                        String substitutionIndexString = sql.substring(idx + myBindVariableSubstitutionBase.length(), endIdx);
462                        int substitutionIndex = Integer.parseInt(substitutionIndexString);
463                        bindVariables.add(myBindVariableValues.get(substitutionIndex));
464
465                        sql = sql.substring(0, idx - 1) + "?" + sql.substring(endIdx + 1);
466                }
467
468                Integer maxResultsToFetch = theMaxResultsToFetch;
469                Integer offset = theOffset;
470                if (offset != null && offset == 0) {
471                        offset = null;
472                }
473                if (maxResultsToFetch != null || offset != null) {
474
475                        maxResultsToFetch = defaultIfNull(maxResultsToFetch, 10000);
476
477                        AbstractLimitHandler limitHandler = (AbstractLimitHandler) myDialect.getLimitHandler();
478                        RowSelection selection = new RowSelection();
479                        selection.setFirstRow(offset);
480                        selection.setMaxRows(maxResultsToFetch);
481                        sql = limitHandler.processSql(sql, selection);
482
483                        int startOfQueryParameterIndex = 0;
484
485                        boolean isSqlServer = (myDialect instanceof SQLServerDialect);
486                        if (isSqlServer) {
487
488                                // The SQLServerDialect has a bunch of one-off processing to deal with rules on when
489                                // a limit can be used, so we can't rely on the flags that the limithandler exposes since
490                                // the exact structure of the query depends on the parameters
491                                if (sql.contains("top(?)")) {
492                                        bindVariables.add(0, maxResultsToFetch);
493                                }
494                                if (sql.contains("offset 0 rows fetch next ? rows only")) {
495                                        bindVariables.add(maxResultsToFetch);
496                                }
497                                if (sql.contains("offset ? rows fetch next ? rows only")) {
498                                        bindVariables.add(theOffset);
499                                        bindVariables.add(maxResultsToFetch);
500                                }
501                                if (offset != null && sql.contains("__row__")) {
502                                        bindVariables.add(theOffset + 1);
503                                        bindVariables.add(theOffset + maxResultsToFetch + 1);
504                                }
505
506                        } else if (limitHandler.supportsVariableLimit()) {
507
508                                boolean bindLimitParametersFirst = limitHandler.bindLimitParametersFirst();
509                                if (limitHandler.useMaxForLimit() && offset != null) {
510                                        maxResultsToFetch = maxResultsToFetch + offset;
511                                }
512
513                                if (limitHandler.bindLimitParametersInReverseOrder()) {
514                                        startOfQueryParameterIndex = bindCountParameter(bindVariables, maxResultsToFetch, limitHandler, startOfQueryParameterIndex, bindLimitParametersFirst);
515                                        bindOffsetParameter(bindVariables, offset, limitHandler, startOfQueryParameterIndex, bindLimitParametersFirst);
516                                } else {
517                                        startOfQueryParameterIndex = bindOffsetParameter(bindVariables, offset, limitHandler, startOfQueryParameterIndex, bindLimitParametersFirst);
518                                        bindCountParameter(bindVariables, maxResultsToFetch, limitHandler, startOfQueryParameterIndex, bindLimitParametersFirst);
519                                }
520                        }
521                }
522
523                return new GeneratedSql(myMatchNothing, sql, bindVariables);
524        }
525
526        private int bindCountParameter(List<Object> bindVariables, Integer maxResultsToFetch, AbstractLimitHandler limitHandler, int startOfQueryParameterIndex, boolean bindLimitParametersFirst) {
527                if (limitHandler.supportsLimit()) {
528                        if (bindLimitParametersFirst) {
529                                bindVariables.add(startOfQueryParameterIndex++, maxResultsToFetch);
530                        } else {
531                                bindVariables.add(maxResultsToFetch);
532                        }
533                }
534                return startOfQueryParameterIndex;
535        }
536
537        public int bindOffsetParameter(List<Object> theBindVariables, @Nullable Integer theOffset, AbstractLimitHandler theLimitHandler, int theStartOfQueryParameterIndex, boolean theBindLimitParametersFirst) {
538                if (theLimitHandler.supportsLimitOffset() && theOffset != null) {
539                        if (theBindLimitParametersFirst) {
540                                theBindVariables.add(theStartOfQueryParameterIndex++, theOffset);
541                        } else {
542                                theBindVariables.add(theOffset);
543                        }
544                }
545                return theStartOfQueryParameterIndex;
546        }
547
548        /**
549         * If at least one predicate builder already exists, return the last one added to the chain. If none has been selected, create a builder on HFJ_RESOURCE, add it and return it.
550         */
551        public BaseJoiningPredicateBuilder getOrCreateFirstPredicateBuilder() {
552                return getOrCreateFirstPredicateBuilder(true);
553        }
554
555        /**
556         * If at least one predicate builder already exists, return the last one added to the chain. If none has been selected, create a builder on HFJ_RESOURCE, add it and return it.
557         */
558        public BaseJoiningPredicateBuilder getOrCreateFirstPredicateBuilder(boolean theIncludeResourceTypeAndNonDeletedFlag) {
559                if (myFirstPredicateBuilder == null) {
560                        getOrCreateResourceTablePredicateBuilder(theIncludeResourceTypeAndNonDeletedFlag);
561                }
562                return myFirstPredicateBuilder;
563        }
564
565        public ResourceTablePredicateBuilder getOrCreateResourceTablePredicateBuilder() {
566                return getOrCreateResourceTablePredicateBuilder(true);
567        }
568
569        public ResourceTablePredicateBuilder getOrCreateResourceTablePredicateBuilder(boolean theIncludeResourceTypeAndNonDeletedFlag) {
570                if (myResourceTableRoot == null) {
571                        ResourceTablePredicateBuilder resourceTable = mySqlBuilderFactory.resourceTable(this);
572                        addTable(resourceTable, null);
573                        if (theIncludeResourceTypeAndNonDeletedFlag) {
574                                Condition typeAndDeletionPredicate = resourceTable.createResourceTypeAndNonDeletedPredicates();
575                                addPredicate(typeAndDeletionPredicate);
576                        }
577                        myResourceTableRoot = resourceTable;
578                }
579                return myResourceTableRoot;
580        }
581
582        /**
583         * The SQL Builder library has one annoying limitation, which is that it does not use/understand bind variables
584         * for its generated SQL. So we work around this by replacing our contents with a string in the SQL consisting
585         * of <code>[random UUID]-[value index]</code> and then
586         */
587        public String generatePlaceholder(Object theValue) {
588                String placeholder = myBindVariableSubstitutionBase + myBindVariableValues.size();
589                myBindVariableValues.add(theValue);
590                return placeholder;
591        }
592
593        public List<String> generatePlaceholders(Collection<?> theValues) {
594                return theValues
595                        .stream()
596                        .map(this::generatePlaceholder)
597                        .collect(Collectors.toList());
598        }
599
600        public int countBindVariables() {
601                return myBindVariableValues.size();
602        }
603
604
605        public void setMatchNothing() {
606                myMatchNothing = true;
607        }
608
609        public DbTable addTable(String theTableName) {
610                return mySchema.addTable(theTableName);
611        }
612
613        public PartitionSettings getPartitionSettings() {
614                return myPartitionSettings;
615        }
616
617        public RequestPartitionId getRequestPartitionId() {
618                return myRequestPartitionId;
619        }
620
621        public String getResourceType() {
622                return myResourceType;
623        }
624
625        public StorageSettings getStorageSettings() {
626                return myStorageSettings;
627        }
628
629        public void addPredicate(@Nonnull Condition theCondition) {
630                assert theCondition != null;
631                mySelect.addCondition(theCondition);
632                myHaveAtLeastOnePredicate = true;
633        }
634
635        public ComboCondition addPredicateLastUpdated(DateRangeParam theDateRange) {
636                ResourceTablePredicateBuilder resourceTableRoot = getOrCreateResourceTablePredicateBuilder(false);
637                List<Condition> conditions = new ArrayList<>(2);
638                BinaryCondition condition;
639
640                if (isNotEqualsComparator(theDateRange)) {
641                        condition = createConditionForValueWithComparator(LESSTHAN, resourceTableRoot.getLastUpdatedColumn(), theDateRange.getLowerBoundAsInstant());
642                        conditions.add(condition);
643                        condition = createConditionForValueWithComparator(GREATERTHAN, resourceTableRoot.getLastUpdatedColumn(), theDateRange.getUpperBoundAsInstant());
644                        conditions.add(condition);
645                        return ComboCondition.or(conditions.toArray(new Condition[0]));
646                }
647
648                if (theDateRange.getLowerBoundAsInstant() != null) {
649                        condition = createConditionForValueWithComparator(GREATERTHAN_OR_EQUALS, resourceTableRoot.getLastUpdatedColumn(), theDateRange.getLowerBoundAsInstant());
650                        conditions.add(condition);
651                }
652
653                if (theDateRange.getUpperBoundAsInstant() != null) {
654                        condition = createConditionForValueWithComparator(LESSTHAN_OR_EQUALS, resourceTableRoot.getLastUpdatedColumn(), theDateRange.getUpperBoundAsInstant());
655                        conditions.add(condition);
656                }
657
658                return ComboCondition.and(conditions.toArray(new Condition[0]));
659        }
660
661        private boolean isNotEqualsComparator(DateRangeParam theDateRange) {
662                if (theDateRange != null) {
663                        DateParam lb = theDateRange.getLowerBound();
664                        DateParam ub = theDateRange.getUpperBound();
665
666                        return lb != null && ub != null && lb.getPrefix().equals(NOT_EQUAL) && ub.getPrefix().equals(NOT_EQUAL);
667                }
668                return false;
669        }
670
671
672        public void addResourceIdsPredicate(List<Long> thePidList) {
673                DbColumn resourceIdColumn = getOrCreateFirstPredicateBuilder().getResourceIdColumn();
674                InCondition predicate = new InCondition(resourceIdColumn, generatePlaceholders(thePidList));
675                addPredicate(predicate);
676        }
677
678        public void excludeResourceIdsPredicate(Set<JpaPid> theExistingPidSetToExclude) {
679
680                // Do  nothing if it's empty
681                if (theExistingPidSetToExclude == null || theExistingPidSetToExclude.isEmpty())
682                        return;
683                
684                List<Long> excludePids = JpaPid.toLongList(theExistingPidSetToExclude);
685                
686                ourLog.trace("excludePids = " + excludePids);
687
688                DbColumn resourceIdColumn = getOrCreateFirstPredicateBuilder().getResourceIdColumn();
689                InCondition predicate = new InCondition(resourceIdColumn, generatePlaceholders(excludePids));
690                predicate.setNegate(true);
691                addPredicate(predicate);
692        }
693
694        public BinaryCondition createConditionForValueWithComparator(ParamPrefixEnum theComparator, DbColumn theColumn, Object theValue) {
695                switch (theComparator) {
696                        case LESSTHAN:
697                                return BinaryCondition.lessThan(theColumn, generatePlaceholder(theValue));
698                        case LESSTHAN_OR_EQUALS:
699                                return BinaryCondition.lessThanOrEq(theColumn, generatePlaceholder(theValue));
700                        case GREATERTHAN:
701                                return BinaryCondition.greaterThan(theColumn, generatePlaceholder(theValue));
702                        case GREATERTHAN_OR_EQUALS:
703                                return BinaryCondition.greaterThanOrEq(theColumn, generatePlaceholder(theValue));
704                        case NOT_EQUAL:
705                                return BinaryCondition.notEqualTo(theColumn, generatePlaceholder(theValue));
706                        case STARTS_AFTER:
707                        case APPROXIMATE:
708                        case ENDS_BEFORE:
709                        case EQUAL:
710                        default:
711                                throw new IllegalArgumentException(Msg.code(1263));
712                }
713        }
714
715        public SearchQueryBuilder newChildSqlBuilder() {
716                return new SearchQueryBuilder(myFhirContext, myStorageSettings, myPartitionSettings, myRequestPartitionId, myResourceType, mySqlBuilderFactory, myBindVariableSubstitutionBase, myDialect, false, myBindVariableValues);
717        }
718
719        public SelectQuery getSelect() {
720                return mySelect;
721        }
722
723        public boolean haveAtLeastOnePredicate() {
724                return myHaveAtLeastOnePredicate;
725        }
726
727        public void addSortCoordsNear(CoordsPredicateBuilder theCoordsBuilder, double theLatitudeValue, double theLongitudeValue, boolean theAscending) {
728                FunctionCall absLatitude = new FunctionCall("ABS");
729                String latitudePlaceholder = generatePlaceholder(theLatitudeValue);
730                ComboExpression absLatitudeMiddle = new ComboExpression(ComboExpression.Op.SUBTRACT, theCoordsBuilder.getColumnLatitude(), latitudePlaceholder);
731                absLatitude = absLatitude.addCustomParams(absLatitudeMiddle);
732
733                FunctionCall absLongitude = new FunctionCall("ABS");
734                String longitudePlaceholder = generatePlaceholder(theLongitudeValue);
735                ComboExpression absLongitudeMiddle = new ComboExpression(ComboExpression.Op.SUBTRACT, theCoordsBuilder.getColumnLongitude(), longitudePlaceholder);
736                absLongitude = absLongitude.addCustomParams(absLongitudeMiddle);
737
738                ComboExpression sum = new ComboExpression(ComboExpression.Op.ADD, absLatitude, absLongitude);
739                String ordering;
740                if (theAscending) {
741                        ordering = "";
742                } else {
743                        ordering = " DESC";
744                }
745
746                String columnName = "MHD" + (myNextNearnessColumnId++);
747                mySelect.addAliasedColumn(sum, columnName);
748                mySelect.addCustomOrderings(columnName + ordering);
749        }
750
751        public void addSortString(DbColumn theColumnValueNormalized, boolean theAscending) {
752                addSortString(theColumnValueNormalized, theAscending, false);
753        }
754
755        public void addSortString(DbColumn theColumnValueNormalized, boolean theAscending, boolean theUseAggregate) {
756                OrderObject.NullOrder nullOrder = OrderObject.NullOrder.LAST;
757                addSortString(theColumnValueNormalized, theAscending, nullOrder, theUseAggregate);
758        }
759
760        public void addSortNumeric(DbColumn theColumnValueNormalized, boolean theAscending) {
761                addSortNumeric(theColumnValueNormalized, theAscending, false);
762        }
763
764        public void addSortNumeric(DbColumn theColumnValueNormalized, boolean theAscending, boolean theUseAggregate) {
765                OrderObject.NullOrder nullOrder = OrderObject.NullOrder.LAST;
766                addSortNumeric(theColumnValueNormalized, theAscending, nullOrder, theUseAggregate);
767        }
768
769        public void addSortDate(DbColumn theColumnValueNormalized, boolean theAscending) {
770                addSortDate(theColumnValueNormalized, theAscending, false);
771        }
772
773        public void addSortDate(DbColumn theColumnValueNormalized, boolean theAscending, boolean theUseAggregate) {
774                OrderObject.NullOrder nullOrder = OrderObject.NullOrder.LAST;
775                addSortDate(theColumnValueNormalized, theAscending, nullOrder, theUseAggregate);
776        }
777
778        public void addSortString(DbColumn theTheColumnValueNormalized, boolean theTheAscending, OrderObject.NullOrder theNullOrder, boolean theUseAggregate) {
779                if ((dialectIsMySql || dialectIsMsSql)) {
780                        // MariaDB, MySQL and MSSQL do not support "NULLS FIRST" and "NULLS LAST" syntax.
781                        String direction = theTheAscending ? " ASC" : " DESC";
782                        String sortColumnName = theTheColumnValueNormalized.getTable().getAlias() + "." + theTheColumnValueNormalized.getName();
783                        final StringBuilder sortColumnNameBuilder = new StringBuilder();
784                        // The following block has been commented out for performance.
785                        // Uncomment if NullOrder is needed for MariaDB, MySQL or MSSQL
786                        /*
787                        // Null values are always treated as less than non-null values.
788                        if ((theTheAscending && theNullOrder == OrderObject.NullOrder.LAST)
789                                || (!theTheAscending && theNullOrder == OrderObject.NullOrder.FIRST)) {
790                                // In this case, precede the "order by" column with a case statement that returns
791                                // 1 for null and 0 non-null so that nulls will be sorted as greater than non-nulls.
792                                sortColumnNameBuilder.append( "CASE WHEN " ).append( sortColumnName ).append( " IS NULL THEN 1 ELSE 0 END" ).append(direction).append(", ");
793                        }
794                   */
795                        sortColumnName = formatColumnNameForAggregate(theTheAscending, theUseAggregate, sortColumnName);
796                        sortColumnNameBuilder.append(sortColumnName).append(direction);
797                        mySelect.addCustomOrderings(sortColumnNameBuilder.toString());
798                } else {
799                        addSort(theTheColumnValueNormalized, theTheAscending, theNullOrder, theUseAggregate);
800                }
801        }
802
803        private static String formatColumnNameForAggregate(boolean theTheAscending, boolean theUseAggregate, String sortColumnName) {
804                if (theUseAggregate) {
805                        String aggregateFunction;
806                        if (theTheAscending) {
807                                aggregateFunction = "MIN";
808                        } else {
809                                aggregateFunction = "MAX";
810                        }
811                        sortColumnName = aggregateFunction + "(" + sortColumnName + ")";
812                }
813                return sortColumnName;
814        }
815
816        public void addSortNumeric(DbColumn theTheColumnValueNormalized, boolean theAscending, OrderObject.NullOrder theNullOrder, boolean theUseAggregate) {
817                if ((dialectIsMySql || dialectIsMsSql)) {
818                        // MariaDB, MySQL and MSSQL do not support "NULLS FIRST" and "NULLS LAST" syntax.
819                        // Null values are always treated as less than non-null values.
820                        // As such special handling is required here.
821                        String direction;
822                        String sortColumnName = theTheColumnValueNormalized.getTable().getAlias() + "." + theTheColumnValueNormalized.getName();
823                        if ((theAscending && theNullOrder == OrderObject.NullOrder.LAST)
824                                || (!theAscending && theNullOrder == OrderObject.NullOrder.FIRST)) {
825                                // Negating the numeric column value and reversing the sort order will ensure that the rows appear
826                                // in the correct order with nulls appearing first or last as needed.
827                                direction = theAscending ? " DESC" : " ASC";
828                                sortColumnName = "-" + sortColumnName;
829                        } else {
830                                direction = theAscending ? " ASC" : " DESC";
831                        }
832                        sortColumnName = formatColumnNameForAggregate(theAscending, theUseAggregate, sortColumnName);
833                        mySelect.addCustomOrderings(sortColumnName + direction);
834                } else {
835                        addSort(theTheColumnValueNormalized, theAscending, theNullOrder, theUseAggregate);
836                }
837        }
838
839        public void addSortDate(DbColumn theTheColumnValueNormalized, boolean theTheAscending, OrderObject.NullOrder theNullOrder, boolean theUseAggregate) {
840                if ((dialectIsMySql || dialectIsMsSql)) {
841                        // MariaDB, MySQL and MSSQL do not support "NULLS FIRST" and "NULLS LAST" syntax.
842                        String direction = theTheAscending ? " ASC" : " DESC";
843                        String sortColumnName = theTheColumnValueNormalized.getTable().getAlias() + "." + theTheColumnValueNormalized.getName();
844                        final StringBuilder sortColumnNameBuilder = new StringBuilder();
845                        // The following block has been commented out for performance.
846                        // Uncomment if NullOrder is needed for MariaDB, MySQL or MSSQL
847                        /*
848                        // Null values are always treated as less than non-null values.
849                        if ((theTheAscending && theNullOrder == OrderObject.NullOrder.LAST)
850                                || (!theTheAscending && theNullOrder == OrderObject.NullOrder.FIRST)) {
851                                // In this case, precede the "order by" column with a case statement that returns
852                                // 1 for null and 0 non-null so that nulls will be sorted as greater than non-nulls.
853                                sortColumnNameBuilder.append( "CASE WHEN " ).append( sortColumnName ).append( " IS NULL THEN 1 ELSE 0 END" ).append(direction).append(", ");
854                        }
855                   */
856                        sortColumnName = formatColumnNameForAggregate(theTheAscending, theUseAggregate, sortColumnName);
857                        sortColumnNameBuilder.append(sortColumnName).append(direction);
858                        mySelect.addCustomOrderings(sortColumnNameBuilder.toString());
859                } else {
860                        addSort(theTheColumnValueNormalized, theTheAscending, theNullOrder, theUseAggregate);
861                }
862        }
863
864        private void addSort(DbColumn theTheColumnValueNormalized, boolean theTheAscending, OrderObject.NullOrder theNullOrder, boolean theUseAggregate) {
865                OrderObject.Dir direction = theTheAscending ? OrderObject.Dir.ASCENDING : OrderObject.Dir.DESCENDING;
866                Object columnToOrder = theTheColumnValueNormalized;
867                if (theUseAggregate) {
868                        if (theTheAscending) {
869                                columnToOrder = FunctionCall.min().addColumnParams(theTheColumnValueNormalized);
870                        } else {
871                                columnToOrder = FunctionCall.max().addColumnParams(theTheColumnValueNormalized);
872                        }
873                }
874                OrderObject orderObject = new OrderObject(direction, columnToOrder);
875                orderObject.setNullOrder(theNullOrder);
876                mySelect.addCustomOrderings(orderObject);
877        }
878
879        /**
880         * If set to true (default is false), force the generated SQL to start
881         * with the {@link ca.uhn.fhir.jpa.model.entity.ResourceTable HFJ_RESOURCE}
882         * table at the root of the query.
883         * <p>
884         * This seems to perform better if there are multiple joins on the
885         * resource ID table.
886         */
887        public void setNeedResourceTableRoot(boolean theNeedResourceTableRoot) {
888                myNeedResourceTableRoot = theNeedResourceTableRoot;
889        }
890}