001/*-
002 * #%L
003 * HAPI FHIR JPA Server
004 * %%
005 * Copyright (C) 2014 - 2025 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.jpa.search.builder.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.NumberPredicateBuilder;
036import ca.uhn.fhir.jpa.search.builder.predicate.QuantityNormalizedPredicateBuilder;
037import ca.uhn.fhir.jpa.search.builder.predicate.QuantityPredicateBuilder;
038import ca.uhn.fhir.jpa.search.builder.predicate.ResourceHistoryPredicateBuilder;
039import ca.uhn.fhir.jpa.search.builder.predicate.ResourceHistoryProvenancePredicateBuilder;
040import ca.uhn.fhir.jpa.search.builder.predicate.ResourceIdPredicateBuilder;
041import ca.uhn.fhir.jpa.search.builder.predicate.ResourceLinkPredicateBuilder;
042import ca.uhn.fhir.jpa.search.builder.predicate.ResourceTablePredicateBuilder;
043import ca.uhn.fhir.jpa.search.builder.predicate.SearchParamPresentPredicateBuilder;
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 jakarta.annotation.Nonnull;
066import jakarta.annotation.Nullable;
067import org.hibernate.dialect.Dialect;
068import org.hibernate.dialect.SQLServerDialect;
069import org.hibernate.dialect.pagination.AbstractLimitHandler;
070import org.hibernate.query.internal.QueryOptionsImpl;
071import org.hibernate.query.spi.Limit;
072import org.hibernate.query.spi.QueryOptions;
073import org.slf4j.Logger;
074import org.slf4j.LoggerFactory;
075
076import java.util.ArrayList;
077import java.util.Collection;
078import java.util.List;
079import java.util.Set;
080import java.util.UUID;
081import java.util.stream.Collectors;
082
083import static ca.uhn.fhir.rest.param.ParamPrefixEnum.GREATERTHAN;
084import static ca.uhn.fhir.rest.param.ParamPrefixEnum.GREATERTHAN_OR_EQUALS;
085import static ca.uhn.fhir.rest.param.ParamPrefixEnum.LESSTHAN;
086import static ca.uhn.fhir.rest.param.ParamPrefixEnum.LESSTHAN_OR_EQUALS;
087import static ca.uhn.fhir.rest.param.ParamPrefixEnum.NOT_EQUAL;
088import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
089
090public class SearchQueryBuilder {
091
092        private static final Logger ourLog = LoggerFactory.getLogger(SearchQueryBuilder.class);
093        private final String myBindVariableSubstitutionBase;
094        private final ArrayList<Object> myBindVariableValues;
095        private final DbSpec mySpec;
096        private final DbSchema mySchema;
097        private final SelectQuery mySelect;
098        private final PartitionSettings myPartitionSettings;
099        private final RequestPartitionId myRequestPartitionId;
100        private final String myResourceType;
101        private final StorageSettings myStorageSettings;
102        private final FhirContext myFhirContext;
103        private final SqlObjectFactory mySqlBuilderFactory;
104        private final boolean myCountQuery;
105        private final Dialect myDialect;
106        private final boolean mySelectPartitionId;
107        private boolean myMatchNothing;
108        private ResourceTablePredicateBuilder myResourceTableRoot;
109        private boolean myHaveAtLeastOnePredicate;
110        private BaseJoiningPredicateBuilder myFirstPredicateBuilder;
111        private boolean dialectIsMsSql;
112        private boolean dialectIsMySql;
113        private boolean myNeedResourceTableRoot;
114        private int myNextNearnessColumnId = 0;
115        private DbColumn mySelectedResourceIdColumn;
116        private DbColumn mySelectedPartitionIdColumn;
117
118        /**
119         * Constructor
120         */
121        public SearchQueryBuilder(
122                        FhirContext theFhirContext,
123                        StorageSettings theStorageSettings,
124                        PartitionSettings thePartitionSettings,
125                        RequestPartitionId theRequestPartitionId,
126                        String theResourceType,
127                        SqlObjectFactory theSqlBuilderFactory,
128                        HibernatePropertiesProvider theDialectProvider,
129                        boolean theCountQuery) {
130                this(
131                                theFhirContext,
132                                theStorageSettings,
133                                thePartitionSettings,
134                                theRequestPartitionId,
135                                theResourceType,
136                                theSqlBuilderFactory,
137                                UUID.randomUUID() + "-",
138                                theDialectProvider.getDialect(),
139                                theCountQuery,
140                                new ArrayList<>(),
141                                thePartitionSettings.isPartitioningEnabled());
142        }
143
144        /**
145         * Constructor for child SQL Builders
146         */
147        private SearchQueryBuilder(
148                        FhirContext theFhirContext,
149                        StorageSettings theStorageSettings,
150                        PartitionSettings thePartitionSettings,
151                        RequestPartitionId theRequestPartitionId,
152                        String theResourceType,
153                        SqlObjectFactory theSqlBuilderFactory,
154                        String theBindVariableSubstitutionBase,
155                        Dialect theDialect,
156                        boolean theCountQuery,
157                        ArrayList<Object> theBindVariableValues,
158                        boolean theSelectPartitionId) {
159                myFhirContext = theFhirContext;
160                myStorageSettings = theStorageSettings;
161                myPartitionSettings = thePartitionSettings;
162                myRequestPartitionId = theRequestPartitionId;
163                myResourceType = theResourceType;
164                mySqlBuilderFactory = theSqlBuilderFactory;
165                myCountQuery = theCountQuery;
166                myDialect = theDialect;
167                if (myDialect instanceof org.hibernate.dialect.MySQLDialect) {
168                        dialectIsMySql = true;
169                }
170                if (myDialect instanceof org.hibernate.dialect.SQLServerDialect) {
171                        dialectIsMsSql = true;
172                }
173
174                mySpec = new DbSpec();
175                mySchema = mySpec.addDefaultSchema();
176                mySelect = new SelectQuery();
177
178                myBindVariableSubstitutionBase = theBindVariableSubstitutionBase;
179                myBindVariableValues = theBindVariableValues;
180                mySelectPartitionId = theSelectPartitionId;
181        }
182
183        public FhirContext getFhirContext() {
184                return myFhirContext;
185        }
186
187        /**
188         * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on a Composite Unique search parameter
189         */
190        public ComboUniqueSearchParameterPredicateBuilder addComboUniquePredicateBuilder() {
191                ComboUniqueSearchParameterPredicateBuilder retVal =
192                                mySqlBuilderFactory.newComboUniqueSearchParameterPredicateBuilder(this);
193                addTable(retVal, null);
194                return retVal;
195        }
196
197        /**
198         * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on a Composite Unique search parameter
199         */
200        public ComboNonUniqueSearchParameterPredicateBuilder addComboNonUniquePredicateBuilder() {
201                ComboNonUniqueSearchParameterPredicateBuilder retVal =
202                                mySqlBuilderFactory.newComboNonUniqueSearchParameterPredicateBuilder(this);
203                addTable(retVal, null);
204                return retVal;
205        }
206
207        /**
208         * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on a COORDS search parameter
209         */
210        public CoordsPredicateBuilder addCoordsPredicateBuilder(@Nullable DbColumn[] theSourceJoinColumn) {
211                CoordsPredicateBuilder retVal = mySqlBuilderFactory.coordsPredicateBuilder(this);
212                addTable(retVal, theSourceJoinColumn);
213                return retVal;
214        }
215
216        /**
217         * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a DATE search parameter
218         */
219        public DatePredicateBuilder addDatePredicateBuilder(@Nullable DbColumn[] theSourceJoinColumn) {
220                DatePredicateBuilder retVal = mySqlBuilderFactory.dateIndexTable(this);
221                addTable(retVal, theSourceJoinColumn);
222                return retVal;
223        }
224
225        /**
226         * Create a predicate builder for selecting on a DATE search parameter
227         */
228        public DatePredicateBuilder createDatePredicateBuilder() {
229                return mySqlBuilderFactory.dateIndexTable(this);
230        }
231
232        /**
233         * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a NUMBER search parameter
234         */
235        public NumberPredicateBuilder addNumberPredicateBuilder(@Nullable DbColumn[] theSourceJoinColumn) {
236                NumberPredicateBuilder retVal = createNumberPredicateBuilder();
237                addTable(retVal, theSourceJoinColumn);
238                return retVal;
239        }
240
241        /**
242         * Create a predicate builder for selecting on a NUMBER search parameter
243         */
244        public NumberPredicateBuilder createNumberPredicateBuilder() {
245                return mySqlBuilderFactory.numberIndexTable(this);
246        }
247
248        /**
249         * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on the Resource table
250         */
251        public ResourceTablePredicateBuilder addResourceTablePredicateBuilder(@Nullable DbColumn[] theSourceJoinColumn) {
252                ResourceTablePredicateBuilder retVal = mySqlBuilderFactory.resourceTable(this);
253                addTable(retVal, theSourceJoinColumn);
254                return retVal;
255        }
256
257        /**
258         * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a QUANTITY search parameter
259         */
260        public QuantityPredicateBuilder addQuantityPredicateBuilder(@Nullable DbColumn[] theSourceJoinColumn) {
261                QuantityPredicateBuilder retVal = createQuantityPredicateBuilder();
262                addTable(retVal, theSourceJoinColumn);
263
264                return retVal;
265        }
266
267        /**
268         * Create a predicate builder for selecting on a QUANTITY search parameter
269         */
270        public QuantityPredicateBuilder createQuantityPredicateBuilder() {
271                return mySqlBuilderFactory.quantityIndexTable(this);
272        }
273
274        public QuantityNormalizedPredicateBuilder addQuantityNormalizedPredicateBuilder(
275                        @Nullable DbColumn[] theSourceJoinColumn) {
276
277                QuantityNormalizedPredicateBuilder retVal = mySqlBuilderFactory.quantityNormalizedIndexTable(this);
278                addTable(retVal, theSourceJoinColumn);
279
280                return retVal;
281        }
282
283        /**
284         * 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
285         */
286        public ResourceHistoryProvenancePredicateBuilder addResourceHistoryProvenancePredicateBuilder(
287                        @Nullable DbColumn[] theSourceJoinColumn, SelectQuery.JoinType theJoinType) {
288                ResourceHistoryProvenancePredicateBuilder retVal =
289                                mySqlBuilderFactory.newResourceHistoryProvenancePredicateBuilder(this);
290                addTable(retVal, theSourceJoinColumn, theJoinType);
291                return retVal;
292        }
293
294        /**
295         * 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
296         */
297        public ResourceHistoryPredicateBuilder addResourceHistoryPredicateBuilder(
298                        @Nullable DbColumn[] theSourceJoinColumn, SelectQuery.JoinType theJoinType) {
299                ResourceHistoryPredicateBuilder retVal = mySqlBuilderFactory.newResourceHistoryPredicateBuilder(this);
300                addTable(retVal, theSourceJoinColumn, theJoinType);
301                return retVal;
302        }
303
304        /**
305         * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a REFERENCE search parameter
306         */
307        public ResourceLinkPredicateBuilder addReferencePredicateBuilder(
308                        QueryStack theQueryStack, @Nullable DbColumn[] theSourceJoinColumn) {
309                ResourceLinkPredicateBuilder retVal = createReferencePredicateBuilder(theQueryStack);
310                addTable(retVal, theSourceJoinColumn);
311                return retVal;
312        }
313
314        /**
315         * Create a predicate builder for selecting on a REFERENCE search parameter
316         */
317        public ResourceLinkPredicateBuilder createReferencePredicateBuilder(QueryStack theQueryStack) {
318                return mySqlBuilderFactory.referenceIndexTable(theQueryStack, this, false);
319        }
320
321        /**
322         * Add and return a predicate builder (or a root query if no root query exists yet) for selecting on a resource link where the
323         * source and target are reversed. This is used for _has queries.
324         */
325        public ResourceLinkPredicateBuilder addReferencePredicateBuilderReversed(
326                        QueryStack theQueryStack, DbColumn[] theSourceJoinColumn) {
327                ResourceLinkPredicateBuilder retVal = mySqlBuilderFactory.referenceIndexTable(theQueryStack, this, true);
328                addTable(retVal, theSourceJoinColumn);
329                return retVal;
330        }
331
332        /**
333         * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a STRING search parameter
334         */
335        public StringPredicateBuilder addStringPredicateBuilder(@Nullable DbColumn[] theSourceJoinColumn) {
336                StringPredicateBuilder retVal = createStringPredicateBuilder();
337                addTable(retVal, theSourceJoinColumn);
338                return retVal;
339        }
340
341        /**
342         * Create a predicate builder for selecting on a STRING search parameter
343         */
344        public StringPredicateBuilder createStringPredicateBuilder() {
345                return mySqlBuilderFactory.stringIndexTable(this);
346        }
347
348        /**
349         * 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
350         */
351        public TagPredicateBuilder addTagPredicateBuilder(@Nullable DbColumn[] theSourceJoinColumn) {
352                TagPredicateBuilder retVal = mySqlBuilderFactory.newTagPredicateBuilder(this);
353                addTable(retVal, theSourceJoinColumn);
354                return retVal;
355        }
356
357        /**
358         * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a TOKEN search parameter
359         */
360        public TokenPredicateBuilder addTokenPredicateBuilder(@Nullable DbColumn[] theSourceJoinColumn) {
361                TokenPredicateBuilder retVal = createTokenPredicateBuilder();
362                addTable(retVal, theSourceJoinColumn);
363                return retVal;
364        }
365
366        /**
367         * Create a predicate builder for selecting on a TOKEN search parameter
368         */
369        public TokenPredicateBuilder createTokenPredicateBuilder() {
370                return mySqlBuilderFactory.tokenIndexTable(this);
371        }
372
373        public void addCustomJoin(
374                        SelectQuery.JoinType theJoinType, DbTable theFromTable, DbTable theToTable, Condition theCondition) {
375                mySelect.addCustomJoin(theJoinType, theFromTable, theToTable, theCondition);
376        }
377
378        public ComboCondition createOnCondition(DbColumn[] theSourceColumn, DbColumn[] theTargetColumn) {
379                ComboCondition onCondition = ComboCondition.and();
380                for (int i = 0; i < theSourceColumn.length; i += 1) {
381                        onCondition.addCondition(BinaryCondition.equalTo(theSourceColumn[i], theTargetColumn[i]));
382                }
383                return onCondition;
384        }
385
386        /**
387         * 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
388         */
389        public SearchParamPresentPredicateBuilder addSearchParamPresentPredicateBuilder(
390                        @Nullable DbColumn[] theSourceJoinColumn) {
391                SearchParamPresentPredicateBuilder retVal = mySqlBuilderFactory.searchParamPresentPredicateBuilder(this);
392                addTable(retVal, theSourceJoinColumn);
393                return retVal;
394        }
395
396        /**
397         * Create, add and return a predicate builder (or a root query if no root query exists yet) for selecting on a URI search parameter
398         */
399        public UriPredicateBuilder addUriPredicateBuilder(@Nullable DbColumn[] theSourceJoinColumn) {
400                UriPredicateBuilder retVal = createUriPredicateBuilder();
401                addTable(retVal, theSourceJoinColumn);
402                return retVal;
403        }
404
405        /**
406         * Create a predicate builder for selecting on a URI search parameter
407         */
408        public UriPredicateBuilder createUriPredicateBuilder() {
409                return mySqlBuilderFactory.uriIndexTable(this);
410        }
411
412        public SqlObjectFactory getSqlBuilderFactory() {
413                return mySqlBuilderFactory;
414        }
415
416        public ResourceIdPredicateBuilder newResourceIdBuilder() {
417                return mySqlBuilderFactory.resourceId(this);
418        }
419
420        /**
421         * Add and return a predicate builder (or a root query if no root query exists yet) for an arbitrary table
422         */
423        private void addTable(BaseJoiningPredicateBuilder thePredicateBuilder, @Nullable DbColumn[] theSourceJoinColumn) {
424                addTable(thePredicateBuilder, theSourceJoinColumn, SelectQuery.JoinType.INNER);
425        }
426
427        private void addTable(
428                        BaseJoiningPredicateBuilder thePredicateBuilder,
429                        @Nullable DbColumn[] theSourceJoinColumns,
430                        SelectQuery.JoinType theJoinType) {
431                if (theSourceJoinColumns != null) {
432                        DbTable fromTable = theSourceJoinColumns[0].getTable();
433                        DbTable toTable = thePredicateBuilder.getTable();
434                        DbColumn[] toColumn = toJoinColumns(thePredicateBuilder);
435                        addJoin(fromTable, toTable, theSourceJoinColumns, toColumn, theJoinType);
436                } else {
437                        if (myFirstPredicateBuilder == null) {
438
439                                BaseJoiningPredicateBuilder root;
440                                if (!myNeedResourceTableRoot) {
441                                        root = thePredicateBuilder;
442                                } else {
443                                        if (thePredicateBuilder instanceof ResourceTablePredicateBuilder) {
444                                                root = thePredicateBuilder;
445                                        } else {
446                                                root = mySqlBuilderFactory.resourceTable(this);
447                                        }
448                                }
449
450                                if (myCountQuery) {
451                                        mySelect.addCustomColumns(
452                                                        FunctionCall.count().setIsDistinct(true).addColumnParams(root.getResourceIdColumn()));
453                                } else {
454                                        if (mySelectPartitionId) {
455                                                mySelectedResourceIdColumn = root.getResourceIdColumn();
456                                                mySelectedPartitionIdColumn = root.getPartitionIdColumn();
457                                                mySelect.addColumns(mySelectedPartitionIdColumn, mySelectedResourceIdColumn);
458                                        } else {
459                                                mySelectedResourceIdColumn = root.getResourceIdColumn();
460                                                mySelect.addColumns(mySelectedResourceIdColumn);
461                                        }
462                                }
463                                mySelect.addFromTable(root.getTable());
464                                myFirstPredicateBuilder = root;
465
466                                if (!myNeedResourceTableRoot || (thePredicateBuilder instanceof ResourceTablePredicateBuilder)) {
467                                        return;
468                                }
469                        }
470
471                        DbTable fromTable = myFirstPredicateBuilder.getTable();
472                        DbTable toTable = thePredicateBuilder.getTable();
473                        DbColumn[] fromColumn = toJoinColumns(myFirstPredicateBuilder);
474                        DbColumn[] toColumn = toJoinColumns(thePredicateBuilder);
475                        addJoin(fromTable, toTable, fromColumn, toColumn, theJoinType);
476                }
477        }
478
479        @Nonnull
480        public DbColumn[] toJoinColumns(BaseJoiningPredicateBuilder theBuilder) {
481                DbColumn partitionIdColumn = theBuilder.getPartitionIdColumn();
482                DbColumn resourceIdColumn = theBuilder.getResourceIdColumn();
483                return toJoinColumns(partitionIdColumn, resourceIdColumn);
484        }
485
486        /**
487         * Remove or keep partition_id columns depending on settings.
488         */
489        @Nonnull
490        public DbColumn[] toJoinColumns(DbColumn partitionIdColumn, DbColumn resourceIdColumn) {
491                if (isIncludePartitionIdInJoins()) {
492                        return new DbColumn[] {partitionIdColumn, resourceIdColumn};
493                } else {
494                        return new DbColumn[] {resourceIdColumn};
495                }
496        }
497
498        public boolean isIncludePartitionIdInJoins() {
499                return mySelectPartitionId && myPartitionSettings.isDatabasePartitionMode();
500        }
501
502        public void addJoin(DbTable theFromTable, DbTable theToTable, DbColumn[] theFromColumn, DbColumn[] theToColumn) {
503                addJoin(theFromTable, theToTable, theFromColumn, theToColumn, SelectQuery.JoinType.INNER);
504        }
505
506        public void addJoin(
507                        DbTable theFromTable,
508                        DbTable theToTable,
509                        DbColumn[] theFromColumn,
510                        DbColumn[] theToColumn,
511                        SelectQuery.JoinType theJoinType) {
512                assert theFromColumn.length == theToColumn.length;
513                Join join = new DbJoin(mySpec, theFromTable, theToTable, theFromColumn, theToColumn);
514                mySelect.addJoins(theJoinType, join);
515        }
516
517        public boolean isSelectPartitionId() {
518                return mySelectPartitionId;
519        }
520
521        /**
522         * Generate and return the SQL generated by this builder
523         */
524        public GeneratedSql generate(@Nullable Integer theOffset, @Nullable Integer theMaxResultsToFetch) {
525                getOrCreateFirstPredicateBuilder();
526
527                mySelect.validate();
528                String sql = mySelect.toString();
529
530                List<Object> bindVariables = new ArrayList<>();
531                while (true) {
532
533                        int idx = sql.indexOf(myBindVariableSubstitutionBase);
534                        if (idx == -1) {
535                                break;
536                        }
537
538                        int endIdx = sql.indexOf("'", idx + myBindVariableSubstitutionBase.length());
539                        String substitutionIndexString = sql.substring(idx + myBindVariableSubstitutionBase.length(), endIdx);
540                        int substitutionIndex = Integer.parseInt(substitutionIndexString);
541                        bindVariables.add(myBindVariableValues.get(substitutionIndex));
542
543                        sql = sql.substring(0, idx - 1) + "?" + sql.substring(endIdx + 1);
544                }
545
546                Integer maxResultsToFetch = theMaxResultsToFetch;
547                Integer offset = theOffset;
548                if (offset != null && offset == 0) {
549                        offset = null;
550                }
551                if (maxResultsToFetch != null || offset != null) {
552
553                        maxResultsToFetch = defaultIfNull(maxResultsToFetch, 10000);
554                        String selectedResourceIdColumn = mySelectedResourceIdColumn.getColumnNameSQL();
555
556                        sql = applyLimitToSql(myDialect, offset, maxResultsToFetch, sql, selectedResourceIdColumn, bindVariables);
557                }
558
559                return new GeneratedSql(myMatchNothing, sql, bindVariables);
560        }
561
562        /**
563         * This method applies the theDialect limiter (select first NNN offset MMM etc etc..) to
564         * a SQL string. It enhances the built-in Hibernate dialect version with some additional
565         * enhancements.
566         */
567        public static String applyLimitToSql(
568                        Dialect theDialect,
569                        Integer theOffset,
570                        Integer theMaxResultsToFetch,
571                        String theInputSql,
572                        @Nullable String theSelectedColumnOrNull,
573                        List<Object> theBindVariables) {
574                AbstractLimitHandler limitHandler = (AbstractLimitHandler) theDialect.getLimitHandler();
575                Limit selection = new Limit();
576                selection.setFirstRow(theOffset);
577                selection.setMaxRows(theMaxResultsToFetch);
578                QueryOptions queryOptions = new QueryOptionsImpl();
579                theInputSql = limitHandler.processSql(theInputSql, selection, queryOptions);
580
581                int startOfQueryParameterIndex = 0;
582
583                boolean isSqlServer = (theDialect instanceof SQLServerDialect);
584                if (isSqlServer) {
585
586                        /*
587                         * SQL server requires an ORDER BY clause to be present in the SQL if there is
588                         * an OFFSET/FETCH FIRST clause, so if there isn't already an ORDER BY clause,
589                         * the theDialect will automatically add an order by with a pseudo-column name. This
590                         * happens in SQLServer2012LimitHandler.
591                         *
592                         * But, SQL Server also pukes if you include an ORDER BY on a column that you
593                         * aren't also SELECTing, if the select statement contains a UNION, INTERSECT or EXCEPT operator.
594                         * Who knows why SQL Server is so picky.. but anyhow, this causes an issue, so we manually replace
595                         * the pseudo-column with an actual selected column.
596                         */
597                        if (theInputSql.contains("order by @@version")) {
598                                if (theSelectedColumnOrNull != null) {
599                                        theInputSql = theInputSql.replace("order by @@version", "order by " + theSelectedColumnOrNull);
600                                } else {
601                                        // not certain if this case can happen, but ordering by the ordinal first column should always
602                                        // be syntactically valid and seems like a better option than ordering by a static value
603                                        // regardless
604                                        theInputSql = theInputSql.replace("order by @@version", "order by 1");
605                                }
606                        }
607
608                        // The SQLServerDialect has a bunch of one-off processing to deal with rules on when
609                        // a limit can be used, so we can't rely on the flags that the limithandler exposes since
610                        // the exact structure of the query depends on the parameters
611                        if (theInputSql.contains("top(?)")) {
612                                theBindVariables.add(0, theMaxResultsToFetch);
613                        }
614                        if (theInputSql.contains("offset 0 rows fetch first ? rows only")) {
615                                theBindVariables.add(theMaxResultsToFetch);
616                        }
617                        if (theInputSql.contains("offset ? rows fetch next ? rows only")) {
618                                theBindVariables.add(theOffset);
619                                theBindVariables.add(theMaxResultsToFetch);
620                        }
621                        if (theOffset != null && theInputSql.contains("rownumber_")) {
622                                theBindVariables.add(theOffset + 1);
623                                theBindVariables.add(theOffset + theMaxResultsToFetch + 1);
624                        }
625
626                } else if (limitHandler.supportsVariableLimit()) {
627
628                        boolean bindLimitParametersFirst = limitHandler.bindLimitParametersFirst();
629                        if (limitHandler.useMaxForLimit() && theOffset != null) {
630                                theMaxResultsToFetch = theMaxResultsToFetch + theOffset;
631                        }
632
633                        if (limitHandler.bindLimitParametersInReverseOrder()) {
634                                startOfQueryParameterIndex = bindCountParameter(
635                                                theBindVariables,
636                                                theMaxResultsToFetch,
637                                                limitHandler,
638                                                startOfQueryParameterIndex,
639                                                bindLimitParametersFirst);
640                                bindOffsetParameter(
641                                                theBindVariables,
642                                                theOffset,
643                                                limitHandler,
644                                                startOfQueryParameterIndex,
645                                                bindLimitParametersFirst);
646                        } else {
647                                startOfQueryParameterIndex = bindOffsetParameter(
648                                                theBindVariables,
649                                                theOffset,
650                                                limitHandler,
651                                                startOfQueryParameterIndex,
652                                                bindLimitParametersFirst);
653                                bindCountParameter(
654                                                theBindVariables,
655                                                theMaxResultsToFetch,
656                                                limitHandler,
657                                                startOfQueryParameterIndex,
658                                                bindLimitParametersFirst);
659                        }
660                }
661                return theInputSql;
662        }
663
664        private static int bindCountParameter(
665                        List<Object> bindVariables,
666                        Integer maxResultsToFetch,
667                        AbstractLimitHandler limitHandler,
668                        int startOfQueryParameterIndex,
669                        boolean bindLimitParametersFirst) {
670                if (limitHandler.supportsLimit()) {
671                        if (bindLimitParametersFirst) {
672                                bindVariables.add(startOfQueryParameterIndex++, maxResultsToFetch);
673                        } else {
674                                bindVariables.add(maxResultsToFetch);
675                        }
676                }
677                return startOfQueryParameterIndex;
678        }
679
680        public static int bindOffsetParameter(
681                        List<Object> theBindVariables,
682                        @Nullable Integer theOffset,
683                        AbstractLimitHandler theLimitHandler,
684                        int theStartOfQueryParameterIndex,
685                        boolean theBindLimitParametersFirst) {
686                if (theLimitHandler.supportsLimitOffset() && theOffset != null) {
687                        if (theBindLimitParametersFirst) {
688                                theBindVariables.add(theStartOfQueryParameterIndex++, theOffset);
689                        } else {
690                                theBindVariables.add(theOffset);
691                        }
692                }
693                return theStartOfQueryParameterIndex;
694        }
695
696        /**
697         * 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.
698         */
699        public BaseJoiningPredicateBuilder getOrCreateFirstPredicateBuilder() {
700                return getOrCreateFirstPredicateBuilder(true);
701        }
702
703        /**
704         * 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.
705         */
706        public BaseJoiningPredicateBuilder getOrCreateFirstPredicateBuilder(
707                        boolean theIncludeResourceTypeAndNonDeletedFlag) {
708                if (myFirstPredicateBuilder == null) {
709                        getOrCreateResourceTablePredicateBuilder(theIncludeResourceTypeAndNonDeletedFlag);
710                }
711                return myFirstPredicateBuilder;
712        }
713
714        public ResourceTablePredicateBuilder getOrCreateResourceTablePredicateBuilder() {
715                return getOrCreateResourceTablePredicateBuilder(true);
716        }
717
718        public ResourceTablePredicateBuilder getOrCreateResourceTablePredicateBuilder(
719                        boolean theIncludeResourceTypeAndNonDeletedFlag) {
720                if (myResourceTableRoot == null) {
721                        ResourceTablePredicateBuilder resourceTable = mySqlBuilderFactory.resourceTable(this);
722                        addTable(resourceTable, null);
723                        if (theIncludeResourceTypeAndNonDeletedFlag) {
724                                Condition typeAndDeletionPredicate = resourceTable.createResourceTypeAndNonDeletedPredicates();
725                                addPredicate(typeAndDeletionPredicate);
726                        }
727                        myResourceTableRoot = resourceTable;
728                }
729                return myResourceTableRoot;
730        }
731
732        /**
733         * The SQL Builder library has one annoying limitation, which is that it does not use/understand bind variables
734         * for its generated SQL. So we work around this by replacing our contents with a string in the SQL consisting
735         * of <code>[random UUID]-[value index]</code> and then
736         */
737        public String generatePlaceholder(Object theValue) {
738                String placeholder = myBindVariableSubstitutionBase + myBindVariableValues.size();
739                myBindVariableValues.add(theValue);
740                return placeholder;
741        }
742
743        public List<String> generatePlaceholders(Collection<?> theValues) {
744                return theValues.stream().map(this::generatePlaceholder).collect(Collectors.toList());
745        }
746
747        public int countBindVariables() {
748                return myBindVariableValues.size();
749        }
750
751        public void setMatchNothing() {
752                myMatchNothing = true;
753        }
754
755        public DbTable addTable(String theTableName) {
756                return mySchema.addTable(theTableName);
757        }
758
759        public PartitionSettings getPartitionSettings() {
760                return myPartitionSettings;
761        }
762
763        public RequestPartitionId getRequestPartitionId() {
764                return myRequestPartitionId;
765        }
766
767        public String getResourceType() {
768                return myResourceType;
769        }
770
771        public StorageSettings getStorageSettings() {
772                return myStorageSettings;
773        }
774
775        public void addPredicate(@Nonnull Condition theCondition) {
776                assert theCondition != null;
777                mySelect.addCondition(theCondition);
778                myHaveAtLeastOnePredicate = true;
779        }
780
781        public ComboCondition addPredicateLastUpdated(DateRangeParam theDateRange) {
782                ResourceTablePredicateBuilder resourceTableRoot = getOrCreateResourceTablePredicateBuilder(false);
783                return addPredicateLastUpdated(theDateRange, resourceTableRoot);
784        }
785
786        public ComboCondition addPredicateLastUpdated(
787                        DateRangeParam theDateRange, ResourceTablePredicateBuilder theResourceTablePredicateBuilder) {
788                List<Condition> conditions = new ArrayList<>(2);
789                BinaryCondition condition;
790
791                if (isNotEqualsComparator(theDateRange)) {
792                        condition = createConditionForValueWithComparator(
793                                        LESSTHAN,
794                                        theResourceTablePredicateBuilder.getLastUpdatedColumn(),
795                                        theDateRange.getLowerBoundAsInstant());
796                        conditions.add(condition);
797                        condition = createConditionForValueWithComparator(
798                                        GREATERTHAN,
799                                        theResourceTablePredicateBuilder.getLastUpdatedColumn(),
800                                        theDateRange.getUpperBoundAsInstant());
801                        conditions.add(condition);
802                        return ComboCondition.or(conditions.toArray(new Condition[0]));
803                }
804
805                if (theDateRange.getLowerBoundAsInstant() != null) {
806                        condition = createConditionForValueWithComparator(
807                                        GREATERTHAN_OR_EQUALS,
808                                        theResourceTablePredicateBuilder.getLastUpdatedColumn(),
809                                        theDateRange.getLowerBoundAsInstant());
810                        conditions.add(condition);
811                }
812
813                if (theDateRange.getUpperBoundAsInstant() != null) {
814                        condition = createConditionForValueWithComparator(
815                                        LESSTHAN_OR_EQUALS,
816                                        theResourceTablePredicateBuilder.getLastUpdatedColumn(),
817                                        theDateRange.getUpperBoundAsInstant());
818                        conditions.add(condition);
819                }
820
821                return ComboCondition.and(conditions.toArray(new Condition[0]));
822        }
823
824        private boolean isNotEqualsComparator(DateRangeParam theDateRange) {
825                if (theDateRange != null) {
826                        DateParam lb = theDateRange.getLowerBound();
827                        DateParam ub = theDateRange.getUpperBound();
828
829                        return lb != null && ub != null && NOT_EQUAL.equals(lb.getPrefix()) && NOT_EQUAL.equals(ub.getPrefix());
830                }
831                return false;
832        }
833
834        public void addResourceIdsPredicate(List<JpaPid> thePidList) {
835                List<Long> pidList = thePidList.stream().map(JpaPid::getId).collect(Collectors.toList());
836
837                DbColumn resourceIdColumn = getOrCreateFirstPredicateBuilder().getResourceIdColumn();
838                InCondition predicate = new InCondition(resourceIdColumn, generatePlaceholders(pidList));
839                addPredicate(predicate);
840        }
841
842        public void excludeResourceIdsPredicate(Set<JpaPid> theExistingPidSetToExclude) {
843
844                // Do  nothing if it's empty
845                if (theExistingPidSetToExclude == null || theExistingPidSetToExclude.isEmpty()) return;
846
847                List<Long> excludePids = JpaPid.toLongList(theExistingPidSetToExclude);
848
849                ourLog.trace("excludePids = {}", excludePids);
850
851                DbColumn resourceIdColumn = getOrCreateFirstPredicateBuilder().getResourceIdColumn();
852                InCondition predicate = new InCondition(resourceIdColumn, generatePlaceholders(excludePids));
853                predicate.setNegate(true);
854                addPredicate(predicate);
855        }
856
857        public BinaryCondition createConditionForValueWithComparator(
858                        ParamPrefixEnum theComparator, DbColumn theColumn, Object theValue) {
859                switch (theComparator) {
860                        case LESSTHAN:
861                                return BinaryCondition.lessThan(theColumn, generatePlaceholder(theValue));
862                        case LESSTHAN_OR_EQUALS:
863                                return BinaryCondition.lessThanOrEq(theColumn, generatePlaceholder(theValue));
864                        case GREATERTHAN:
865                                return BinaryCondition.greaterThan(theColumn, generatePlaceholder(theValue));
866                        case GREATERTHAN_OR_EQUALS:
867                                return BinaryCondition.greaterThanOrEq(theColumn, generatePlaceholder(theValue));
868                        case NOT_EQUAL:
869                                return BinaryCondition.notEqualTo(theColumn, generatePlaceholder(theValue));
870                        case EQUAL:
871                                // NB: fhir searches are always range searches;
872                                // which is why we do not use "EQUAL"
873                        case STARTS_AFTER:
874                        case APPROXIMATE:
875                        case ENDS_BEFORE:
876                        default:
877                                throw new IllegalArgumentException(Msg.code(1263));
878                }
879        }
880
881        public SearchQueryBuilder newChildSqlBuilder(boolean theSelectPartitionId) {
882                return new SearchQueryBuilder(
883                                myFhirContext,
884                                myStorageSettings,
885                                myPartitionSettings,
886                                myRequestPartitionId,
887                                myResourceType,
888                                mySqlBuilderFactory,
889                                myBindVariableSubstitutionBase,
890                                myDialect,
891                                false,
892                                myBindVariableValues,
893                                theSelectPartitionId);
894        }
895
896        public SelectQuery getSelect() {
897                return mySelect;
898        }
899
900        public boolean haveAtLeastOnePredicate() {
901                return myHaveAtLeastOnePredicate;
902        }
903
904        public void addSortCoordsNear(
905                        CoordsPredicateBuilder theCoordsBuilder,
906                        double theLatitudeValue,
907                        double theLongitudeValue,
908                        boolean theAscending) {
909                FunctionCall absLatitude = new FunctionCall("ABS");
910                String latitudePlaceholder = generatePlaceholder(theLatitudeValue);
911                ComboExpression absLatitudeMiddle = new ComboExpression(
912                                ComboExpression.Op.SUBTRACT, theCoordsBuilder.getColumnLatitude(), latitudePlaceholder);
913                absLatitude = absLatitude.addCustomParams(absLatitudeMiddle);
914
915                FunctionCall absLongitude = new FunctionCall("ABS");
916                String longitudePlaceholder = generatePlaceholder(theLongitudeValue);
917                ComboExpression absLongitudeMiddle = new ComboExpression(
918                                ComboExpression.Op.SUBTRACT, theCoordsBuilder.getColumnLongitude(), longitudePlaceholder);
919                absLongitude = absLongitude.addCustomParams(absLongitudeMiddle);
920
921                ComboExpression sum = new ComboExpression(ComboExpression.Op.ADD, absLatitude, absLongitude);
922                String ordering;
923                if (theAscending) {
924                        ordering = "";
925                } else {
926                        ordering = " DESC";
927                }
928
929                String columnName = "MHD" + (myNextNearnessColumnId++);
930                mySelect.addAliasedColumn(sum, columnName);
931                mySelect.addCustomOrderings(columnName + ordering);
932        }
933
934        public void addSortString(DbColumn theColumnValueNormalized, boolean theAscending) {
935                addSortString(theColumnValueNormalized, theAscending, false);
936        }
937
938        public void addSortString(DbColumn theColumnValueNormalized, boolean theAscending, boolean theUseAggregate) {
939                OrderObject.NullOrder nullOrder = OrderObject.NullOrder.LAST;
940                addSortString(theColumnValueNormalized, theAscending, nullOrder, theUseAggregate);
941        }
942
943        public void addSortNumeric(DbColumn theColumnValueNormalized, boolean theAscending) {
944                addSortNumeric(theColumnValueNormalized, theAscending, false);
945        }
946
947        public void addSortNumeric(DbColumn theColumnValueNormalized, boolean theAscending, boolean theUseAggregate) {
948                OrderObject.NullOrder nullOrder = OrderObject.NullOrder.LAST;
949                addSortNumeric(theColumnValueNormalized, theAscending, nullOrder, theUseAggregate);
950        }
951
952        public void addSortDate(DbColumn theColumnValueNormalized, boolean theAscending) {
953                addSortDate(theColumnValueNormalized, theAscending, false);
954        }
955
956        public void addSortDate(DbColumn theColumnValueNormalized, boolean theAscending, boolean theUseAggregate) {
957                OrderObject.NullOrder nullOrder = OrderObject.NullOrder.LAST;
958                addSortDate(theColumnValueNormalized, theAscending, nullOrder, theUseAggregate);
959        }
960
961        public void addSortString(
962                        DbColumn theTheColumnValueNormalized,
963                        boolean theTheAscending,
964                        OrderObject.NullOrder theNullOrder,
965                        boolean theUseAggregate) {
966                if ((dialectIsMySql || dialectIsMsSql)) {
967                        // MariaDB, MySQL and MSSQL do not support "NULLS FIRST" and "NULLS LAST" syntax.
968                        String direction = theTheAscending ? " ASC" : " DESC";
969                        String sortColumnName =
970                                        theTheColumnValueNormalized.getTable().getAlias() + "." + theTheColumnValueNormalized.getName();
971                        final StringBuilder sortColumnNameBuilder = new StringBuilder();
972                        // The following block has been commented out for performance.
973                        // Uncomment if NullOrder is needed for MariaDB, MySQL or MSSQL
974                        /*
975                        // Null values are always treated as less than non-null values.
976                        if ((theTheAscending && theNullOrder == OrderObject.NullOrder.LAST)
977                                || (!theTheAscending && theNullOrder == OrderObject.NullOrder.FIRST)) {
978                                // In this case, precede the "order by" column with a case statement that returns
979                                // 1 for null and 0 non-null so that nulls will be sorted as greater than non-nulls.
980                                sortColumnNameBuilder.append( "CASE WHEN " ).append( sortColumnName ).append( " IS NULL THEN 1 ELSE 0 END" ).append(direction).append(", ");
981                        }
982                        */
983                        sortColumnName = formatColumnNameForAggregate(theTheAscending, theUseAggregate, sortColumnName);
984                        sortColumnNameBuilder.append(sortColumnName).append(direction);
985                        mySelect.addCustomOrderings(sortColumnNameBuilder.toString());
986                } else {
987                        addSort(theTheColumnValueNormalized, theTheAscending, theNullOrder, theUseAggregate);
988                }
989        }
990
991        private static String formatColumnNameForAggregate(
992                        boolean theTheAscending, boolean theUseAggregate, String sortColumnName) {
993                if (theUseAggregate) {
994                        String aggregateFunction;
995                        if (theTheAscending) {
996                                aggregateFunction = "MIN";
997                        } else {
998                                aggregateFunction = "MAX";
999                        }
1000                        sortColumnName = aggregateFunction + "(" + sortColumnName + ")";
1001                }
1002                return sortColumnName;
1003        }
1004
1005        public void addSortNumeric(
1006                        DbColumn theTheColumnValueNormalized,
1007                        boolean theAscending,
1008                        OrderObject.NullOrder theNullOrder,
1009                        boolean theUseAggregate) {
1010                if ((dialectIsMySql || dialectIsMsSql)) {
1011                        // MariaDB, MySQL and MSSQL do not support "NULLS FIRST" and "NULLS LAST" syntax.
1012                        // Null values are always treated as less than non-null values.
1013                        // As such special handling is required here.
1014                        String direction;
1015                        String sortColumnName =
1016                                        theTheColumnValueNormalized.getTable().getAlias() + "." + theTheColumnValueNormalized.getName();
1017                        if ((theAscending && theNullOrder == OrderObject.NullOrder.LAST)
1018                                        || (!theAscending && theNullOrder == OrderObject.NullOrder.FIRST)) {
1019                                // Negating the numeric column value and reversing the sort order will ensure that the rows appear
1020                                // in the correct order with nulls appearing first or last as needed.
1021                                direction = theAscending ? " DESC" : " ASC";
1022                                sortColumnName = "-" + sortColumnName;
1023                        } else {
1024                                direction = theAscending ? " ASC" : " DESC";
1025                        }
1026                        sortColumnName = formatColumnNameForAggregate(theAscending, theUseAggregate, sortColumnName);
1027                        mySelect.addCustomOrderings(sortColumnName + direction);
1028                } else {
1029                        addSort(theTheColumnValueNormalized, theAscending, theNullOrder, theUseAggregate);
1030                }
1031        }
1032
1033        public void addSortDate(
1034                        DbColumn theTheColumnValueNormalized,
1035                        boolean theTheAscending,
1036                        OrderObject.NullOrder theNullOrder,
1037                        boolean theUseAggregate) {
1038                if ((dialectIsMySql || dialectIsMsSql)) {
1039                        // MariaDB, MySQL and MSSQL do not support "NULLS FIRST" and "NULLS LAST" syntax.
1040                        String direction = theTheAscending ? " ASC" : " DESC";
1041                        String sortColumnName =
1042                                        theTheColumnValueNormalized.getTable().getAlias() + "." + theTheColumnValueNormalized.getName();
1043                        final StringBuilder sortColumnNameBuilder = new StringBuilder();
1044                        // The following block has been commented out for performance.
1045                        // Uncomment if NullOrder is needed for MariaDB, MySQL or MSSQL
1046                        /*
1047                        // Null values are always treated as less than non-null values.
1048                        if ((theTheAscending && theNullOrder == OrderObject.NullOrder.LAST)
1049                                || (!theTheAscending && theNullOrder == OrderObject.NullOrder.FIRST)) {
1050                                // In this case, precede the "order by" column with a case statement that returns
1051                                // 1 for null and 0 non-null so that nulls will be sorted as greater than non-nulls.
1052                                sortColumnNameBuilder.append( "CASE WHEN " ).append( sortColumnName ).append( " IS NULL THEN 1 ELSE 0 END" ).append(direction).append(", ");
1053                        }
1054                        */
1055                        sortColumnName = formatColumnNameForAggregate(theTheAscending, theUseAggregate, sortColumnName);
1056                        sortColumnNameBuilder.append(sortColumnName).append(direction);
1057                        mySelect.addCustomOrderings(sortColumnNameBuilder.toString());
1058                } else {
1059                        addSort(theTheColumnValueNormalized, theTheAscending, theNullOrder, theUseAggregate);
1060                }
1061        }
1062
1063        private void addSort(
1064                        DbColumn theTheColumnValueNormalized,
1065                        boolean theTheAscending,
1066                        OrderObject.NullOrder theNullOrder,
1067                        boolean theUseAggregate) {
1068                OrderObject.Dir direction = theTheAscending ? OrderObject.Dir.ASCENDING : OrderObject.Dir.DESCENDING;
1069                Object columnToOrder = theTheColumnValueNormalized;
1070                if (theUseAggregate) {
1071                        if (theTheAscending) {
1072                                columnToOrder = FunctionCall.min().addColumnParams(theTheColumnValueNormalized);
1073                        } else {
1074                                columnToOrder = FunctionCall.max().addColumnParams(theTheColumnValueNormalized);
1075                        }
1076                }
1077                OrderObject orderObject = new OrderObject(direction, columnToOrder);
1078                orderObject.setNullOrder(theNullOrder);
1079                mySelect.addCustomOrderings(orderObject);
1080        }
1081
1082        /**
1083         * If set to true (default is false), force the generated SQL to start
1084         * with the {@link ca.uhn.fhir.jpa.model.entity.ResourceTable HFJ_RESOURCE}
1085         * table at the root of the query.
1086         * <p>
1087         * This seems to perform better if there are multiple joins on the
1088         * resource ID table.
1089         */
1090        public void setNeedResourceTableRoot(boolean theNeedResourceTableRoot) {
1091                myNeedResourceTableRoot = theNeedResourceTableRoot;
1092        }
1093}