private async Task <SearchResult> SearchImpl(SqlSearchOptions sqlSearchOptions, SqlSearchType searchType, string currentSearchParameterHash, CancellationToken cancellationToken)
        {
            Expression searchExpression = sqlSearchOptions.Expression;

            // AND in the continuation token
            if (!string.IsNullOrWhiteSpace(sqlSearchOptions.ContinuationToken) && !sqlSearchOptions.CountOnly)
            {
                var continuationToken = ContinuationToken.FromString(sqlSearchOptions.ContinuationToken);
                if (continuationToken != null)
                {
                    if (string.IsNullOrEmpty(continuationToken.SortValue))
                    {
                        // Check whether it's a _lastUpdated or (_type,_lastUpdated) sort optimization
                        bool optimize = true;

                        (SearchParameterInfo searchParamInfo, SortOrder sortOrder) = sqlSearchOptions.Sort.Count == 0 ? default : sqlSearchOptions.Sort[0];
                                                                                     if (sqlSearchOptions.Sort.Count > 0)
                                                                                     {
                                                                                         if (!(searchParamInfo.Name == SearchParameterNames.LastUpdated || searchParamInfo.Name == SearchParameterNames.ResourceType))
                                                                                         {
                                                                                             optimize = false;
                                                                                         }
                                                                                     }

                                                                                     FieldName           fieldName;
                                                                                     object              keyValue;
                                                                                     SearchParameterInfo parameter;
                                                                                     if (continuationToken.ResourceTypeId == null || _schemaInformation.Current < SchemaVersionConstants.PartitionedTables)
                                                                                     {
                                                                                         // backwards compat
                                                                                         parameter = SqlSearchParameters.ResourceSurrogateIdParameter;
                                                                                         fieldName = SqlFieldName.ResourceSurrogateId;
                                                                                         keyValue  = continuationToken.ResourceSurrogateId;
                                                                                     }
                                                                                     else
                                                                                     {
                                                                                         parameter = SqlSearchParameters.PrimaryKeyParameter;
                                                                                         fieldName = SqlFieldName.PrimaryKey;
                                                                                         keyValue  = new PrimaryKeyValue(continuationToken.ResourceTypeId.Value, continuationToken.ResourceSurrogateId);
                                                                                     }

                                                                                     Expression lastUpdatedExpression = null;
                                                                                     if (!optimize)
                                                                                     {
                                                                                         lastUpdatedExpression = Expression.GreaterThan(fieldName, null, keyValue);
                                                                                     }
                                                                                     else
                                                                                     {
                                                                                         if (sortOrder == SortOrder.Ascending)
                                                                                         {
                                                                                             lastUpdatedExpression = Expression.GreaterThan(fieldName, null, keyValue);
                                                                                         }
                                                                                         else
                                                                                         {
                                                                                             lastUpdatedExpression = Expression.LessThan(fieldName, null, keyValue);
                                                                                         }
                                                                                     }

                                                                                     var tokenExpression = Expression.SearchParameter(parameter, lastUpdatedExpression);
                                                                                     searchExpression = searchExpression == null ? tokenExpression : Expression.And(tokenExpression, searchExpression);
                    }
                }
                else
                {
                    throw new BadRequestException(Resources.InvalidContinuationToken);
                }
            }

            var originalSort        = new List <(SearchParameterInfo, SortOrder)>(sqlSearchOptions.Sort);
            var clonedSearchOptions = UpdateSort(sqlSearchOptions, searchExpression, searchType);

            if (clonedSearchOptions.CountOnly)
            {
                // if we're only returning a count, discard any _include parameters since included resources are not counted.
                searchExpression = searchExpression?.AcceptVisitor(RemoveIncludesRewriter.Instance);
            }

            SqlRootExpression expression = (SqlRootExpression)searchExpression
                                           ?.AcceptVisitor(LastUpdatedToResourceSurrogateIdRewriter.Instance)
                                           .AcceptVisitor(DateTimeEqualityRewriter.Instance)
                                           .AcceptVisitor(FlatteningRewriter.Instance)
                                           .AcceptVisitor(UntypedReferenceRewriter.Instance)
                                           .AcceptVisitor(_sqlRootExpressionRewriter)
                                           .AcceptVisitor(_partitionEliminationRewriter)
                                           .AcceptVisitor(_sortRewriter, clonedSearchOptions)
                                           .AcceptVisitor(SearchParamTableExpressionReorderer.Instance)
                                           .AcceptVisitor(MissingSearchParamVisitor.Instance)
                                           .AcceptVisitor(NotExpressionRewriter.Instance)
                                           .AcceptVisitor(_chainFlatteningRewriter)
                                           .AcceptVisitor(ResourceColumnPredicatePushdownRewriter.Instance)
                                           .AcceptVisitor(DateTimeBoundedRangeRewriter.Instance)
                                           .AcceptVisitor(
                (SqlExpressionRewriterWithInitialContext <object>)(_schemaInformation.Current >= SchemaVersionConstants.PartitionedTables
                                                       ? StringOverflowRewriter.Instance
                                                       : LegacyStringOverflowRewriter.Instance))
                                           .AcceptVisitor(NumericRangeRewriter.Instance)
                                           .AcceptVisitor(IncludeMatchSeedRewriter.Instance)
                                           .AcceptVisitor(TopRewriter.Instance, clonedSearchOptions)
                                           .AcceptVisitor(IncludeRewriter.Instance)
                                           ?? SqlRootExpression.WithResourceTableExpressions();

            using (SqlConnectionWrapper sqlConnectionWrapper = await _sqlConnectionWrapperFactory.ObtainSqlConnectionWrapperAsync(cancellationToken, true))
                using (SqlCommandWrapper sqlCommandWrapper = sqlConnectionWrapper.CreateSqlCommand())
                {
                    var stringBuilder = new IndentedStringBuilder(new StringBuilder());

                    EnableTimeAndIoMessageLogging(stringBuilder, sqlConnectionWrapper);

                    var queryGenerator = new SqlQueryGenerator(
                        stringBuilder,
                        new HashingSqlQueryParameterManager(new SqlQueryParameterManager(sqlCommandWrapper.Parameters)),
                        _model,
                        searchType,
                        _schemaInformation,
                        currentSearchParameterHash);

                    expression.AcceptVisitor(queryGenerator, clonedSearchOptions);

                    sqlCommandWrapper.CommandText = stringBuilder.ToString();

                    LogSqlCommand(sqlCommandWrapper);

                    using (var reader = await sqlCommandWrapper.ExecuteReaderAsync(CommandBehavior.SequentialAccess, cancellationToken))
                    {
                        if (clonedSearchOptions.CountOnly)
                        {
                            await reader.ReadAsync(cancellationToken);

                            long count = reader.GetInt64(0);
                            if (count > int.MaxValue)
                            {
                                _requestContextAccessor.RequestContext.BundleIssues.Add(
                                    new OperationOutcomeIssue(
                                        OperationOutcomeConstants.IssueSeverity.Error,
                                        OperationOutcomeConstants.IssueType.NotSupported,
                                        string.Format(Core.Resources.SearchCountResultsExceedLimit, count, int.MaxValue)));

                                throw new InvalidSearchOperationException(string.Format(Core.Resources.SearchCountResultsExceedLimit, count, int.MaxValue));
                            }

                            var searchResult = new SearchResult((int)count, clonedSearchOptions.UnsupportedSearchParams);

                            // call NextResultAsync to get the info messages
                            await reader.NextResultAsync(cancellationToken);

                            return(searchResult);
                        }

                        var   resources           = new List <SearchResultEntry>(sqlSearchOptions.MaxItemCount);
                        short?newContinuationType = null;
                        long? newContinuationId   = null;
                        bool  moreResults         = false;
                        int   matchCount          = 0;

                        string sortValue           = null;
                        var    isResultPartial     = false;
                        int    numberOfColumnsRead = 0;

                        while (await reader.ReadAsync(cancellationToken))
                        {
                            PopulateResourceTableColumnsToRead(
                                reader,
                                out short resourceTypeId,
                                out string resourceId,
                                out int version,
                                out bool isDeleted,
                                out long resourceSurrogateId,
                                out string requestMethod,
                                out bool isMatch,
                                out bool isPartialEntry,
                                out bool isRawResourceMetaSet,
                                out string searchParameterHash,
                                out Stream rawResourceStream);
                            numberOfColumnsRead = reader.FieldCount;

                            // If we get to this point, we know there are more results so we need a continuation token
                            // Additionally, this resource shouldn't be included in the results
                            if (matchCount >= clonedSearchOptions.MaxItemCount && isMatch)
                            {
                                moreResults = true;

                                continue;
                            }

                            string rawResource;
                            using (rawResourceStream)
                            {
                                rawResource = await _compressedRawResourceConverter.ReadCompressedRawResource(rawResourceStream);
                            }

                            // See if this resource is a continuation token candidate and increase the count
                            if (isMatch)
                            {
                                newContinuationType = resourceTypeId;
                                newContinuationId   = resourceSurrogateId;

                                // For normal queries, we select _defaultNumberOfColumnsReadFromResult number of columns.
                                // If we have more, that means we have an extra column tracking sort value.
                                // Keep track of sort value if this is the last row.
                                if (matchCount == clonedSearchOptions.MaxItemCount - 1 && reader.FieldCount > _defaultNumberOfColumnsReadFromResult)
                                {
                                    var tempSortValue = reader.GetValue(SortValueColumnName);
                                    if ((tempSortValue as DateTime?) != null)
                                    {
                                        sortValue = (tempSortValue as DateTime?).Value.ToString("o");
                                    }
                                    else
                                    {
                                        sortValue = tempSortValue.ToString();
                                    }
                                }

                                matchCount++;
                            }

                            // as long as at least one entry was marked as partial, this resultset
                            // should be marked as partial
                            isResultPartial = isResultPartial || isPartialEntry;

                            resources.Add(new SearchResultEntry(
                                              new ResourceWrapper(
                                                  resourceId,
                                                  version.ToString(CultureInfo.InvariantCulture),
                                                  _model.GetResourceTypeName(resourceTypeId),
                                                  new RawResource(rawResource, FhirResourceFormat.Json, isMetaSet: isRawResourceMetaSet),
                                                  new ResourceRequest(requestMethod),
                                                  new DateTimeOffset(ResourceSurrogateIdHelper.ResourceSurrogateIdToLastUpdated(resourceSurrogateId), TimeSpan.Zero),
                                                  isDeleted,
                                                  null,
                                                  null,
                                                  null,
                                                  searchParameterHash),
                                              isMatch ? SearchEntryMode.Match : SearchEntryMode.Include));
                        }

                        // call NextResultAsync to get the info messages
                        await reader.NextResultAsync(cancellationToken);

                        ContinuationToken continuationToken =
                            moreResults
                            ? new ContinuationToken(
                                clonedSearchOptions.Sort.Select(s =>
                                                                s.searchParameterInfo.Name switch
                        {
                            SearchParameterNames.ResourceType => (object)newContinuationType,
                            SearchParameterNames.LastUpdated => newContinuationId,
                            _ => sortValue,
                        }).ToArray())
        public override async Task <SearchResult> SearchAsync(SearchOptions searchOptions, CancellationToken cancellationToken)
        {
            SqlSearchOptions sqlSearchOptions = new SqlSearchOptions(searchOptions);
            SearchResult     searchResult     = await SearchImpl(sqlSearchOptions, SqlSearchType.Default, null, cancellationToken);

            int resultCount = searchResult.Results.Count();

            if (!sqlSearchOptions.IsSortWithFilter &&
                searchResult.ContinuationToken == null &&
                resultCount <= sqlSearchOptions.MaxItemCount &&
                sqlSearchOptions.Sort != null &&
                sqlSearchOptions.Sort.Count > 0 &&
                sqlSearchOptions.Sort[0].searchParameterInfo.Code != KnownQueryParameterNames.LastUpdated)
            {
                // We seem to have run a sort which has returned less results than what max we can return.
                // Let's determine whether we need to execute another query or not.
                if ((sqlSearchOptions.Sort[0].sortOrder == SortOrder.Ascending && sqlSearchOptions.DidWeSearchForSortValue.HasValue && !sqlSearchOptions.DidWeSearchForSortValue.Value) ||
                    (sqlSearchOptions.Sort[0].sortOrder == SortOrder.Descending && sqlSearchOptions.DidWeSearchForSortValue.HasValue && sqlSearchOptions.DidWeSearchForSortValue.Value))
                {
                    if (sqlSearchOptions.MaxItemCount - resultCount == 0)
                    {
                        // Since we are already returning MaxItemCount number of resources we don't want
                        // to execute another search right now just to drop all the resources. We will return
                        // a "special" ct so that we the subsequent request will be handled correctly.
                        var ct = new ContinuationToken(new object[]
                        {
                            SqlSearchConstants.SortSentinelValueForCt,
                            0,
                        });

                        searchResult = new SearchResult(searchResult.Results, ct.ToJson(), searchResult.SortOrder, searchResult.UnsupportedSearchParameters);
                    }
                    else
                    {
                        var finalResultsInOrder = new List <SearchResultEntry>();
                        finalResultsInOrder.AddRange(searchResult.Results);
                        sqlSearchOptions.SortQuerySecondPhase = true;
                        sqlSearchOptions.MaxItemCount        -= resultCount;

                        searchResult = await SearchImpl(sqlSearchOptions, SqlSearchType.Default, null, cancellationToken);

                        finalResultsInOrder.AddRange(searchResult.Results);
                        searchResult = new SearchResult(
                            finalResultsInOrder,
                            searchResult.ContinuationToken,
                            searchResult.SortOrder,
                            searchResult.UnsupportedSearchParameters);
                    }
                }
            }

            // If we should include the total count of matching search results
            if (sqlSearchOptions.IncludeTotal == TotalType.Accurate && !sqlSearchOptions.CountOnly)
            {
                // If this is the first page and there aren't any more pages
                if (sqlSearchOptions.ContinuationToken == null && searchResult.ContinuationToken == null)
                {
                    // Count the match results on the page.
                    searchResult.TotalCount = searchResult.Results.Count(r => r.SearchEntryMode == SearchEntryMode.Match);
                }
                else
                {
                    try
                    {
                        // Otherwise, indicate that we'd like to get the count
                        sqlSearchOptions.CountOnly = true;

                        // And perform a second read.
                        var countOnlySearchResult = await SearchImpl(sqlSearchOptions, SqlSearchType.Default, null, cancellationToken);

                        searchResult.TotalCount = countOnlySearchResult.TotalCount;
                    }
                    finally
                    {
                        // Ensure search options is set to its original state.
                        sqlSearchOptions.CountOnly = false;
                    }
                }
            }

            return(searchResult);
        }
        protected override async Task <SearchResult> SearchHistoryInternalAsync(SearchOptions searchOptions, CancellationToken cancellationToken)
        {
            SqlSearchOptions sqlSearchOptions = new SqlSearchOptions(searchOptions);

            return(await SearchImpl(sqlSearchOptions, SqlSearchType.History, null, cancellationToken));
        }