private async Task <SearchResult> SearchImpl(SearchOptions searchOptions, bool historySearch, CancellationToken cancellationToken) { Expression searchExpression = searchOptions.Expression; // AND in the continuation token if (!string.IsNullOrWhiteSpace(searchOptions.ContinuationToken) && !searchOptions.CountOnly) { var continuationToken = ContinuationToken.FromString(searchOptions.ContinuationToken); if (continuationToken != null) { // in case it's a _lastUpdated sort optimization if (string.IsNullOrEmpty(continuationToken.SortValue)) { (SearchParameterInfo searchParamInfo, SortOrder sortOrder) = searchOptions.GetFirstSupportedSortParam(); Expression lastUpdatedExpression = sortOrder == SortOrder.Ascending ? Expression.GreaterThan(SqlFieldName.ResourceSurrogateId, null, continuationToken.ResourceSurrogateId) : Expression.LessThan(SqlFieldName.ResourceSurrogateId, null, continuationToken.ResourceSurrogateId); var tokenExpression = Expression.SearchParameter(SqlSearchParameters.ResourceSurrogateIdParameter, lastUpdatedExpression); searchExpression = searchExpression == null ? tokenExpression : (Expression)Expression.And(tokenExpression, searchExpression); } } else { throw new BadRequestException(Resources.InvalidContinuationToken); } } if (searchOptions.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(_sqlRootExpressionRewriter) .AcceptVisitor(_sortRewriter, searchOptions) .AcceptVisitor(DenormalizedPredicateRewriter.Instance) .AcceptVisitor(NormalizedPredicateReorderer.Instance) .AcceptVisitor(_chainFlatteningRewriter) .AcceptVisitor(DateTimeBoundedRangeRewriter.Instance) .AcceptVisitor(_stringOverflowRewriter) .AcceptVisitor(NumericRangeRewriter.Instance) .AcceptVisitor(MissingSearchParamVisitor.Instance) .AcceptVisitor(IncludeDenormalizedRewriter.Instance) .AcceptVisitor(TopRewriter.Instance, searchOptions) .AcceptVisitor(IncludeRewriter.Instance) ?? SqlRootExpression.WithDenormalizedExpressions(); using (SqlConnectionWrapper sqlConnectionWrapper = _sqlConnectionWrapperFactory.ObtainSqlConnectionWrapper(true)) using (SqlCommandWrapper sqlCommandWrapper = sqlConnectionWrapper.CreateSqlCommand()) { var stringBuilder = new IndentedStringBuilder(new StringBuilder()); EnableTimeAndIoMessageLogging(stringBuilder, sqlConnectionWrapper); var queryGenerator = new SqlQueryGenerator(stringBuilder, new SqlQueryParameterManager(sqlCommandWrapper.Parameters), _model, historySearch, _schemaInformation); expression.AcceptVisitor(queryGenerator, searchOptions); sqlCommandWrapper.CommandText = stringBuilder.ToString(); LogSqlCommand(sqlCommandWrapper); using (var reader = await sqlCommandWrapper.ExecuteReaderAsync(CommandBehavior.SequentialAccess, cancellationToken)) { if (searchOptions.CountOnly) { await reader.ReadAsync(cancellationToken); return(new SearchResult(reader.GetInt32(0), searchOptions.UnsupportedSearchParams)); } var resources = new List <SearchResultEntry>(searchOptions.MaxItemCount); long?newContinuationId = null; bool moreResults = false; int matchCount = 0; // Currently we support only date time sort type. DateTime?sortValue = null; while (await reader.ReadAsync(cancellationToken)) { (short resourceTypeId, string resourceId, int version, bool isDeleted, long resourceSurrogateId, string requestMethod, bool isMatch, bool isPartialEntry, bool isRawResourceMetaSet, Stream rawResourceStream) = reader.ReadRow( VLatest.Resource.ResourceTypeId, VLatest.Resource.ResourceId, VLatest.Resource.Version, VLatest.Resource.IsDeleted, VLatest.Resource.ResourceSurrogateId, VLatest.Resource.RequestMethod, _isMatch, _isPartial, VLatest.Resource.IsRawResourceMetaSet, VLatest.Resource.RawResource); // 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 >= searchOptions.MaxItemCount && isMatch) { moreResults = true; // At this point we are at the last row. // if we have more columns, it means sort expressions were added. if (reader.FieldCount > 10) { sortValue = reader.GetValue(SortValueColumnName) as DateTime?; } continue; } // See if this resource is a continuation token candidate and increase the count if (isMatch) { newContinuationId = resourceSurrogateId; matchCount++; } string rawResource; using (rawResourceStream) using (var gzipStream = new GZipStream(rawResourceStream, CompressionMode.Decompress)) using (var streamReader = new StreamReader(gzipStream, SqlServerFhirDataStore.ResourceEncoding)) { rawResource = await streamReader.ReadToEndAsync(); } // 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), isMatch ? SearchEntryMode.Match : SearchEntryMode.Include)); } // call NextResultAsync to get the info messages await reader.NextResultAsync(cancellationToken); IReadOnlyList <(string parameterName, string reason)> unsupportedSortingParameters; if (searchOptions.Sort?.Count > 0) { unsupportedSortingParameters = searchOptions .UnsupportedSortingParams .Concat(searchOptions.Sort .Where(x => !x.searchParameterInfo.IsSortSupported()) .Select(s => (s.searchParameterInfo.Name, Core.Resources.SortNotSupported))).ToList(); } else { unsupportedSortingParameters = searchOptions.UnsupportedSortingParams; } // Continuation token prep ContinuationToken continuationToken = null; if (moreResults) { if (sortValue.HasValue) { continuationToken = new ContinuationToken(new object[] { sortValue.Value.ToString("o"), newContinuationId ?? 0, }); } else { continuationToken = new ContinuationToken(new object[] { newContinuationId ?? 0, }); } } return(new SearchResult(resources, searchOptions.UnsupportedSearchParams, unsupportedSortingParameters, continuationToken?.ToJson(), _isResultPartial)); } } }