diff --git a/src/Microsoft.Health.Fhir.SqlServer.UnitTests/Features/Search/SqlServerSearchServiceOptimizationTests.cs b/src/Microsoft.Health.Fhir.SqlServer.UnitTests/Features/Search/SqlServerSearchServiceOptimizationTests.cs deleted file mode 100644 index fc9da41ae2..0000000000 --- a/src/Microsoft.Health.Fhir.SqlServer.UnitTests/Features/Search/SqlServerSearchServiceOptimizationTests.cs +++ /dev/null @@ -1,122 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. -// ------------------------------------------------------------------------------------------------- - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Health.Fhir.Core.Features.Search; -using Microsoft.Health.Fhir.Core.Models; -using Microsoft.Health.Fhir.Tests.Common; -using Microsoft.Health.Test.Utilities; -using Xunit; - -namespace Microsoft.Health.Fhir.SqlServer.UnitTests.Features.Search -{ - /// - /// Unit tests for SqlServerSearchService optimization logic. - /// These tests verify the early return conditions for the optimization method. - /// - [Trait(Traits.OwningTeam, OwningTeam.Fhir)] - [Trait(Traits.Category, Categories.DataSourceValidation)] - public class SqlServerSearchServiceOptimizationTests - { - [Fact] - public void SearchOptions_WithCountOnly_DisqualifiesOptimization() - { - // Arrange - var searchOptions = new SearchOptions - { - ResourceVersionTypes = ResourceVersionType.Latest, - CountOnly = true, - }; - - // Act & Assert - // When CountOnly is true, the optimization should be skipped - Assert.True(searchOptions.CountOnly); - Assert.Equal(ResourceVersionType.Latest, searchOptions.ResourceVersionTypes); - } - - [Fact] - public void SearchOptions_WithHistoryVersionType_QualifiesForOptimization() - { - // Arrange - var searchOptions = new SearchOptions - { - ResourceVersionTypes = ResourceVersionType.History, - CountOnly = false, - }; - - // Act & Assert - // History version type should now qualify for optimization (for versioned reads) - Assert.False(searchOptions.CountOnly); - Assert.True(searchOptions.ResourceVersionTypes.HasFlag(ResourceVersionType.History)); - } - - [Fact] - public void SearchOptions_WithSoftDeletedVersionType_DisqualifiesOptimization() - { - // Arrange - var searchOptions = new SearchOptions - { - ResourceVersionTypes = ResourceVersionType.SoftDeleted, - CountOnly = false, - }; - - // Act & Assert - // SoftDeleted version type alone should disqualify optimization - Assert.False(searchOptions.CountOnly); - Assert.Equal(ResourceVersionType.SoftDeleted, searchOptions.ResourceVersionTypes); - Assert.False(searchOptions.ResourceVersionTypes.HasFlag(ResourceVersionType.History)); - Assert.NotEqual(ResourceVersionType.Latest, searchOptions.ResourceVersionTypes); - } - - [Fact] - public void SearchOptions_WithOptimalConditions_QualifiesForOptimization() - { - // Arrange - var searchOptions = new SearchOptions - { - ResourceVersionTypes = ResourceVersionType.Latest, - CountOnly = false, - }; - - // Act & Assert - // When all conditions are met, the optimization should proceed - Assert.False(searchOptions.CountOnly); - Assert.Equal(ResourceVersionType.Latest, searchOptions.ResourceVersionTypes); - } - - [Fact] - public void ResourceVersionType_Latest_IsExpectedValue() - { - // Act & Assert - // Verify that ResourceVersionType.Latest has the expected flag value - Assert.True((ResourceVersionType.Latest & ResourceVersionType.Latest) == ResourceVersionType.Latest); - } - - [Fact] - public void ResourceVersionType_History_IsDifferentFromLatest() - { - // Act & Assert - // Verify that History and Latest are different version types - Assert.NotEqual(ResourceVersionType.Latest, ResourceVersionType.History); - } - - [Fact] - public void SearchOptions_WithCombinedVersionTypes_QualifiesForOptimization() - { - // Arrange - Combined Latest and History should qualify for optimization - var searchOptions = new SearchOptions - { - ResourceVersionTypes = ResourceVersionType.Latest | ResourceVersionType.History, - CountOnly = false, - }; - - // Act & Assert - Assert.False(searchOptions.CountOnly); - Assert.True(searchOptions.ResourceVersionTypes.HasFlag(ResourceVersionType.History)); - Assert.True(searchOptions.ResourceVersionTypes.HasFlag(ResourceVersionType.Latest)); - } - } -} diff --git a/src/Microsoft.Health.Fhir.SqlServer/Features/Search/SqlServerSearchService.cs b/src/Microsoft.Health.Fhir.SqlServer/Features/Search/SqlServerSearchService.cs index 9db134e76a..189e98f5dc 100644 --- a/src/Microsoft.Health.Fhir.SqlServer/Features/Search/SqlServerSearchService.cs +++ b/src/Microsoft.Health.Fhir.SqlServer/Features/Search/SqlServerSearchService.cs @@ -499,6 +499,13 @@ await _sqlRetryService.ExecuteSql( searchResult = simpleReadResult; return; } + else if (await TryHandleMultipleResourceReadsAsync(expression, clonedSearchOptions, connection, sqlCommand, cancellationToken) is SearchResult multipleReadResult) + { + // Multiple resource IDs - handled using GetResources stored procedure + _logger.LogInformation("Multiple resource read using GetResources stored procedure."); + searchResult = multipleReadResult; + return; + } else { var stringBuilder = new IndentedStringBuilder(new StringBuilder()); @@ -929,6 +936,214 @@ private static bool TryExtractSimpleStringValue(Expression expression, out strin return false; } + /// + /// Attempts to handle multiple resource read requests using the GetResources stored procedure. + /// This provides better performance than generated SQL for multiple resource lookups. + /// + /// The SQL root expression to analyze + /// Search options for the request + /// The SQL connection + /// The SQL command to populate and execute + /// Cancellation token + /// SearchResult if this was a multiple resource read, null otherwise + private async Task TryHandleMultipleResourceReadsAsync(SqlRootExpression expression, SqlSearchOptions searchOptions, SqlConnection connection, SqlCommand command, CancellationToken cancellationToken) + { + // Only optimize for non-count queries without sorting or includes operations + if (searchOptions.CountOnly || + searchOptions.IncludeTotal != TotalType.None || + searchOptions.IsIncludesOperation) + { + return null; + } + + // Only optimize for Latest searches (no history) + if (searchOptions.ResourceVersionTypes != ResourceVersionType.Latest) + { + return null; + } + + // Ensure we have exactly two resource table expressions (one for resource type, one for resource ID) + if (expression.ResourceTableExpressions.Count != 2 || + expression.SearchParamTableExpressions.Count > 0) + { + return null; + } + + // Extract resource type and IDs from the expressions + if (!TryExtractResourceTypeAndIds(expression, out string resourceType, out List resourceIds)) + { + return null; + } + + // Must have multiple IDs for this optimization + if (resourceIds == null || resourceIds.Count == 0) + { + return null; + } + + // Get resource type ID + if (!_model.TryGetResourceTypeId(resourceType, out short resourceTypeId)) + { + return null; + } + + try + { + // Create a DataTable for the table-valued parameter + var resourceKeysTable = new DataTable(); + resourceKeysTable.Columns.Add("ResourceTypeId", typeof(short)); + resourceKeysTable.Columns.Add("ResourceId", typeof(string)); + resourceKeysTable.Columns.Add("Version", typeof(int)); + + // Populate the table with resource keys + foreach (var resourceId in resourceIds) + { + resourceKeysTable.Rows.Add(resourceTypeId, resourceId, DBNull.Value); + } + + // Populate command to use GetResources stored procedure + command.CommandType = CommandType.StoredProcedure; + command.CommandText = "dbo.GetResources"; + command.Connection = connection; + + var parameter = command.Parameters.AddWithValue("@ResourceKeys", resourceKeysTable); + parameter.SqlDbType = SqlDbType.Structured; + parameter.TypeName = "dbo.ResourceKeyList"; + + LogSqlCommand(command); + + // Execute and read results + var results = new List(); + + using (var reader = await command.ExecuteReaderAsync(CommandBehavior.SequentialAccess, cancellationToken)) + { + while (await reader.ReadAsync(cancellationToken)) + { + // GetResources returns: ResourceTypeId, ResourceId, ResourceSurrogateId, Version, IsDeleted, IsHistory, RawResource, IsRawResourceMetaSet, SearchParamHash + short resTypeId = reader.GetInt16(0); // ResourceTypeId + string resId = reader.GetString(1); // ResourceId + long resourceSurrogateId = reader.GetInt64(2); // ResourceSurrogateId + int version = reader.GetInt32(3); // Version + bool isDeleted = reader.GetBoolean(4); // IsDeleted + bool isHistory = reader.GetBoolean(5); // IsHistory + byte[] rawResourceBytes = reader.GetSqlBytes(6).Value; // RawResource + bool isRawResourceMetaSet = reader.GetBoolean(7); // IsRawResourceMetaSet + string searchParameterHash = await reader.IsDBNullAsync(8, cancellationToken) ? null : reader.GetString(8); // SearchParamHash + + // Skip deleted or history records + if (isDeleted || isHistory) + { + continue; + } + + Lazy rawResource = new Lazy(() => + { + using var rawResourceStream = new MemoryStream(rawResourceBytes); + var decompressedResource = _compressedRawResourceConverter.ReadCompressedRawResource(rawResourceStream); + + if (string.IsNullOrEmpty(decompressedResource)) + { + decompressedResource = MissingResourceFactory.CreateJson(resId, _model.GetResourceTypeName(resTypeId), "warning", "incomplete"); + _requestContextAccessor.SetMissingResourceCode(System.Net.HttpStatusCode.PartialContent); + } + + return decompressedResource; + }); + + var searchResultEntry = new SearchResultEntry( + new ResourceWrapper( + resId, + version.ToString(CultureInfo.InvariantCulture), + _model.GetResourceTypeName(resTypeId), + searchOptions.OnlyIds ? null : new RawResource(rawResource, FhirResourceFormat.Json, isMetaSet: isRawResourceMetaSet), + new ResourceRequest("GET"), + resourceSurrogateId.ToLastUpdated(), + isDeleted, + null, + null, + null, + searchParameterHash, + resourceSurrogateId), + SearchEntryMode.Match); + + results.Add(searchResultEntry); + } + } + + return new SearchResult(results, null, null, searchOptions.UnsupportedSearchParams); + } + catch + { + // If anything goes wrong, fall back to SQL generation + return null; + } + } + + /// + /// Attempts to extract resource type and multiple IDs from an expression with IN clause. + /// + /// The SQL root expression to analyze + /// The extracted resource type + /// The extracted list of resource IDs + /// True if extraction was successful, false otherwise + private static bool TryExtractResourceTypeAndIds(SqlRootExpression expression, out string resourceType, out List resourceIds) + { + resourceType = null; + resourceIds = null; + + // Find resource type and ID expressions in ResourceTableExpressions + SearchParameterExpression resourceTypeExpression = null; + SearchParameterExpression resourceIdExpression = null; + + foreach (var searchParamExpression in expression.ResourceTableExpressions.OfType()) + { + if (searchParamExpression.Parameter.Name == SearchParameterNames.ResourceType) + { + resourceTypeExpression = searchParamExpression; + } + else if (searchParamExpression.Parameter.Name == SearchParameterNames.Id) + { + resourceIdExpression = searchParamExpression; + } + } + + if (resourceTypeExpression == null || resourceIdExpression == null) + { + return false; + } + + // Extract resource type + if (!TryExtractSimpleStringValue(resourceTypeExpression.Expression, out resourceType)) + { + return false; + } + + // Check if the ID expression is an OR expression with multiple values + if (resourceIdExpression.Expression is MultiaryExpression multiaryExpression) + { + // Extract all IDs from the OR expression + resourceIds = new List(); + foreach (var subExpression in multiaryExpression.Expressions) + { + if (TryExtractSimpleStringValue(subExpression, out string idValue)) + { + resourceIds.Add(idValue); + } + else + { + // If any sub-expression is not a simple string, fail the extraction + resourceIds.Clear(); + return false; + } + } + + return resourceIds.Count > 0; + } + + // Not a multi-ID expression + return false; + } + /// /// Searches for resources by their type and surrogate id and optionally a searchParamHash and will return resources /// @@ -2108,7 +2323,7 @@ private void ProcessPredicateForStats( break; default: - // Non-search-parameter leaf (e.g. compartment) – ignore for stats + // Non-search-parameter leaf (e.g. compartment) � ignore for stats break; } } diff --git a/test/Microsoft.Health.Fhir.Shared.Tests.Integration/Microsoft.Health.Fhir.Shared.Tests.Integration.projitems b/test/Microsoft.Health.Fhir.Shared.Tests.Integration/Microsoft.Health.Fhir.Shared.Tests.Integration.projitems index 85b445bbf2..497901c15b 100644 --- a/test/Microsoft.Health.Fhir.Shared.Tests.Integration/Microsoft.Health.Fhir.Shared.Tests.Integration.projitems +++ b/test/Microsoft.Health.Fhir.Shared.Tests.Integration/Microsoft.Health.Fhir.Shared.Tests.Integration.projitems @@ -31,6 +31,7 @@ + diff --git a/test/Microsoft.Health.Fhir.Shared.Tests.Integration/Persistence/SqlServerSearchServiceResourceReadOptimizationTests.cs b/test/Microsoft.Health.Fhir.Shared.Tests.Integration/Persistence/SqlServerSearchServiceResourceReadOptimizationTests.cs new file mode 100644 index 0000000000..4c7df0d6cf --- /dev/null +++ b/test/Microsoft.Health.Fhir.Shared.Tests.Integration/Persistence/SqlServerSearchServiceResourceReadOptimizationTests.cs @@ -0,0 +1,293 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Hl7.Fhir.Model; +using Microsoft.Health.Fhir.Core.Extensions; +using Microsoft.Health.Fhir.Core.Features.Search; +using Microsoft.Health.Fhir.Core.Models; +using Microsoft.Health.Fhir.Tests.Common; +using Microsoft.Health.Fhir.Tests.Common.FixtureParameters; +using Microsoft.Health.Test.Utilities; +using Xunit; +using Xunit.Abstractions; +using Task = System.Threading.Tasks.Task; + +namespace Microsoft.Health.Fhir.Tests.Integration.Persistence +{ + /// + /// Integration tests for SqlServerSearchService optimization for single and multiple resource ID reads. + /// These tests verify that the optimized code paths correctly handle: + /// 1. Single resource reads by type and ID + /// 2. Multiple resource reads by type and multiple IDs + /// 3. Versioned resource reads (_history) + /// 4. Edge cases like deleted resources, missing resources, and mixed scenarios + /// + [FhirStorageTestsFixtureArgumentSets(DataStore.SqlServer)] + [Trait(Traits.OwningTeam, OwningTeam.Fhir)] + [Trait(Traits.Category, Categories.DataSourceValidation)] + public class SqlServerSearchServiceResourceReadOptimizationTests : IClassFixture + { + private readonly FhirStorageTestsFixture _fixture; + private readonly ITestOutputHelper _output; + + public SqlServerSearchServiceResourceReadOptimizationTests(FhirStorageTestsFixture fixture, ITestOutputHelper testOutputHelper) + { + _fixture = fixture; + _output = testOutputHelper; + } + + [Fact] + public async Task GivenASingleResourceId_WhenSearchedByTypeAndId_ThenOptimizedPathReturnsCorrectResource() + { + // Arrange - Create a test patient + var patient = Samples.GetJsonSample("Patient").ToPoco(); + patient.Id = Guid.NewGuid().ToString(); + patient.Name = new List + { + new HumanName { Family = "TestSingle", Given = new[] { "Single" } }, + }; + + var saveResult = await _fixture.Mediator.UpsertResourceAsync(patient.ToResourceElement()); + + // Act - Search by _id only (resource type is already specified in SearchAsync) + var query = new List> + { + new Tuple("_id", saveResult.RawResourceElement.Id), + }; + + var searchResult = await _fixture.SearchService.SearchAsync("Patient", query, CancellationToken.None); + + // Assert + Assert.NotNull(searchResult); + Assert.Single(searchResult.Results); + Assert.Equal(saveResult.RawResourceElement.Id, searchResult.Results.First().Resource.ResourceId); + Assert.Equal("Patient", searchResult.Results.First().Resource.ResourceTypeName); + _output.WriteLine($"Successfully retrieved single resource: Patient/{saveResult.RawResourceElement.Id}"); + } + + [Fact] + public async Task GivenMultipleResourceIds_WhenSearchedByTypeAndIds_ThenOptimizedPathReturnsAllResources() + { + // Arrange - Create multiple test patients + var patient1 = Samples.GetJsonSample("Patient").ToPoco(); + patient1.Id = Guid.NewGuid().ToString(); + patient1.Name = new List { new HumanName { Family = "TestMulti1" } }; + + var patient2 = Samples.GetJsonSample("Patient").ToPoco(); + patient2.Id = Guid.NewGuid().ToString(); + patient2.Name = new List { new HumanName { Family = "TestMulti2" } }; + + var patient3 = Samples.GetJsonSample("Patient").ToPoco(); + patient3.Id = Guid.NewGuid().ToString(); + patient3.Name = new List { new HumanName { Family = "TestMulti3" } }; + + var saveResult1 = await _fixture.Mediator.UpsertResourceAsync(patient1.ToResourceElement()); + var saveResult2 = await _fixture.Mediator.UpsertResourceAsync(patient2.ToResourceElement()); + var saveResult3 = await _fixture.Mediator.UpsertResourceAsync(patient3.ToResourceElement()); + + // Act - Search by multiple _id values + var idList = $"{saveResult1.RawResourceElement.Id},{saveResult2.RawResourceElement.Id},{saveResult3.RawResourceElement.Id}"; + var query = new List> + { + new Tuple("_id", idList), + }; + + var searchResult = await _fixture.SearchService.SearchAsync("Patient", query, CancellationToken.None); + + // Assert + Assert.NotNull(searchResult); + Assert.Equal(3, searchResult.Results.Count()); + + var returnedIds = searchResult.Results.Select(r => r.Resource.ResourceId).ToHashSet(); + Assert.Contains(saveResult1.RawResourceElement.Id, returnedIds); + Assert.Contains(saveResult2.RawResourceElement.Id, returnedIds); + Assert.Contains(saveResult3.RawResourceElement.Id, returnedIds); + + _output.WriteLine($"Successfully retrieved {searchResult.Results.Count()} resources"); + } + + [Fact] + public async Task GivenMultipleResourceIdsWithSomeMissing_WhenSearched_ThenOnlyExistingResourcesReturned() + { + // Arrange - Create two patients and use one non-existent ID + var patient1 = Samples.GetJsonSample("Patient").ToPoco(); + patient1.Id = Guid.NewGuid().ToString(); + patient1.Name = new List { new HumanName { Family = "TestPartial1" } }; + + var patient2 = Samples.GetJsonSample("Patient").ToPoco(); + patient2.Id = Guid.NewGuid().ToString(); + patient2.Name = new List { new HumanName { Family = "TestPartial2" } }; + + var saveResult1 = await _fixture.Mediator.UpsertResourceAsync(patient1.ToResourceElement()); + var saveResult2 = await _fixture.Mediator.UpsertResourceAsync(patient2.ToResourceElement()); + var nonExistentId = Guid.NewGuid().ToString(); + + // Act - Search with mix of existing and non-existent IDs + var idList = $"{saveResult1.RawResourceElement.Id},{nonExistentId},{saveResult2.RawResourceElement.Id}"; + var query = new List> + { + new Tuple("_id", idList), + }; + + var searchResult = await _fixture.SearchService.SearchAsync("Patient", query, CancellationToken.None); + + // Assert - Only existing resources should be returned + Assert.NotNull(searchResult); + Assert.Equal(2, searchResult.Results.Count()); + + var returnedIds = searchResult.Results.Select(r => r.Resource.ResourceId).ToHashSet(); + Assert.Contains(saveResult1.RawResourceElement.Id, returnedIds); + Assert.Contains(saveResult2.RawResourceElement.Id, returnedIds); + Assert.DoesNotContain(nonExistentId, returnedIds); + + _output.WriteLine($"Successfully filtered out non-existent resource and returned {searchResult.Results.Count()} valid resources"); + } + + [Fact] + public async Task GivenASingleResourceIdWithVersionedRead_WhenSearched_ThenCorrectVersionReturned() + { + // Arrange - Create a patient and update it to create versions + var patient = Samples.GetJsonSample("Patient").ToPoco(); + patient.Id = Guid.NewGuid().ToString(); + patient.Name = new List { new HumanName { Family = "TestVersion", Given = new[] { "Version1" } } }; + + var saveResult = await _fixture.Mediator.UpsertResourceAsync(patient.ToResourceElement()); + + // Update to create version 2 + patient.Name[0].Given = new[] { "Version2" }; + var updateResult = await _fixture.Mediator.UpsertResourceAsync(patient.ToResourceElement(), Core.Features.Persistence.WeakETag.FromVersionId(saveResult.RawResourceElement.VersionId)); + + // Act - Search for specific version using history + var query = new List> + { + new Tuple("_id", saveResult.RawResourceElement.Id), + }; + + // Search with History version type to test versioned read optimization + var searchResult = await _fixture.SearchService.SearchAsync( + "Patient", + query, + CancellationToken.None, + resourceVersionTypes: ResourceVersionType.Latest | ResourceVersionType.History); + + // Assert + Assert.NotNull(searchResult); + Assert.NotEmpty(searchResult.Results); + Assert.All(searchResult.Results, r => Assert.Equal(saveResult.RawResourceElement.Id, r.Resource.ResourceId)); + + _output.WriteLine($"Successfully retrieved versioned resource: Patient/{saveResult.RawResourceElement.Id}"); + } + + [Fact] + public async Task GivenMultipleResourceIdsWithDeletedResource_WhenSearched_ThenOnlyActiveResourcesReturned() + { + // Arrange - Create patients and delete one + var patient1 = Samples.GetJsonSample("Patient").ToPoco(); + patient1.Id = Guid.NewGuid().ToString(); + patient1.Name = new List { new HumanName { Family = "TestDeleted1" } }; + + var patient2 = Samples.GetJsonSample("Patient").ToPoco(); + patient2.Id = Guid.NewGuid().ToString(); + patient2.Name = new List { new HumanName { Family = "TestDeleted2" } }; + + var patient3 = Samples.GetJsonSample("Patient").ToPoco(); + patient3.Id = Guid.NewGuid().ToString(); + patient3.Name = new List { new HumanName { Family = "TestDeleted3" } }; + + var saveResult1 = await _fixture.Mediator.UpsertResourceAsync(patient1.ToResourceElement()); + var saveResult2 = await _fixture.Mediator.UpsertResourceAsync(patient2.ToResourceElement()); + var saveResult3 = await _fixture.Mediator.UpsertResourceAsync(patient3.ToResourceElement()); + + // Delete patient2 + await _fixture.Mediator.DeleteResourceAsync(new Core.Features.Persistence.ResourceKey("Patient", saveResult2.RawResourceElement.Id), Core.Messages.Delete.DeleteOperation.SoftDelete); + + // Act - Search including the deleted resource ID + var idList = $"{saveResult1.RawResourceElement.Id},{saveResult2.RawResourceElement.Id},{saveResult3.RawResourceElement.Id}"; + var query = new List> + { + new Tuple("_id", idList), + }; + + var searchResult = await _fixture.SearchService.SearchAsync("Patient", query, CancellationToken.None); + + // Assert - Deleted resource should not be returned (default is Latest only) + Assert.NotNull(searchResult); + Assert.Equal(2, searchResult.Results.Count()); + + var returnedIds = searchResult.Results.Select(r => r.Resource.ResourceId).ToHashSet(); + Assert.Contains(saveResult1.RawResourceElement.Id, returnedIds); + Assert.DoesNotContain(saveResult2.RawResourceElement.Id, returnedIds); // Deleted + Assert.Contains(saveResult3.RawResourceElement.Id, returnedIds); + + _output.WriteLine($"Successfully excluded deleted resource, returned {searchResult.Results.Count()} active resources"); + } + + [Fact] + public async Task GivenLargeNumberOfResourceIds_WhenSearched_ThenOptimizationHandlesEfficiently() + { + // Arrange - Create 10 patients + var createdIds = new List(); + for (int i = 0; i < 10; i++) + { + var patient = Samples.GetJsonSample("Patient").ToPoco(); + patient.Id = Guid.NewGuid().ToString(); + patient.Name = new List { new HumanName { Family = $"TestLarge{i}" } }; + + var saveResult = await _fixture.Mediator.UpsertResourceAsync(patient.ToResourceElement()); + createdIds.Add(saveResult.RawResourceElement.Id); + } + + // Act - Search for all 10 resources + var idList = string.Join(",", createdIds); + var query = new List> + { + new Tuple("_id", idList), + }; + + var searchResult = await _fixture.SearchService.SearchAsync("Patient", query, CancellationToken.None); + + // Assert + Assert.NotNull(searchResult); + Assert.Equal(10, searchResult.Results.Count()); + + var returnedIds = searchResult.Results.Select(r => r.Resource.ResourceId).ToHashSet(); + foreach (var id in createdIds) + { + Assert.Contains(id, returnedIds); + } + + _output.WriteLine($"Successfully retrieved all {searchResult.Results.Count()} resources efficiently"); + } + + [Fact] + public async Task GivenSearchWithCountOnly_WhenSearched_ThenOptimizationDoesNotApply() + { + // Arrange + var patient = Samples.GetJsonSample("Patient").ToPoco(); + patient.Id = Guid.NewGuid().ToString(); + patient.Name = new List { new HumanName { Family = "TestCount" } }; + + var saveResult = await _fixture.Mediator.UpsertResourceAsync(patient.ToResourceElement()); + + // Act - Search with count only + var query = new List> + { + new Tuple("_id", saveResult.RawResourceElement.Id), + new Tuple("_summary", "count"), + }; + + var searchResult = await _fixture.SearchService.SearchAsync("Patient", query, CancellationToken.None); + + // Assert + Assert.NotNull(searchResult); + _output.WriteLine($"Count-only search completed successfully with total: {searchResult.TotalCount}"); + } + } +}