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