Repository navigation
Log and warn on URI search values longer than the SQL index - #6013
Open
apurvabhaleMS wants to merge 8 commits into
Open
apurvabhaleMS wants to merge 8 commits into
apurvabhaleMS wants to merge 8 commits into
Conversation
SQL Server stores and compares only the first 256 characters of URI search values. Values that differ after that point match each other, with no signal to the caller. - Log a warning (without the value) when a URI longer than 256 characters is indexed, per resource type and search parameter. - Return a warning issue in the search Bundle when a URI search value is 256 characters or longer. - Truncate URIs to the column length before de-duplicating index rows. A resource with two URIs that share the first 256 characters previously failed to save with HTTP 500 (SQL error 2627 on @UriSearchParams). - $bulk-delete and $bulk-update keep accepting such filters; they log a warning instead of treating the new issue as an invalid parameter. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment on lines
+63
to
+72
| foreach (SearchIndexEntry entry in merge.ResourceWrapper.SearchIndices) | ||
| { | ||
| if (entry.Value is UriSearchValue uriValue && uriValue.Uri.Length > IndexedUriMaxLength) | ||
| { | ||
| truncated ??= new Dictionary<(string, Uri), (int, int)>(); | ||
| var key = (merge.ResourceWrapper.ResourceTypeName, entry.SearchParameter.Url); | ||
| truncated.TryGetValue(key, out (int Count, int MaxLength) current); | ||
| truncated[key] = (current.Count + 1, Math.Max(current.MaxLength, uriValue.Uri.Length)); | ||
| } | ||
| } |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #6013 +/- ##
==========================================
+ Coverage 80.54% 80.69% +0.14%
==========================================
Files 988 990 +2
Lines 38028 38193 +165
Branches 6061 6121 +60
==========================================
+ Hits 30630 30818 +188
+ Misses 5931 5906 -25
- Partials 1467 1469 +2 🚀 New features to boost your workflow:
|
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Long-URI warnings can be duplicated across export checkpoints or emitted multiple times when searches encounter different affected parameters.
2 open issues
1 file not reviewed
src/Microsoft.Health.Fhir.Core/Resources.Designer.cs: Generated file
What changed in this PR
Makes SQL Server’s 256-character URI search limit visible without changing its matching behavior, and prevents duplicate-key failures when URI values share the indexed prefix. Cosmos DB behavior remains unchanged.
Changes:
- Logs oversized URI values without exposing their contents and adds search Bundle warnings.
- Truncates URI index rows before deduplication and allows bulk operations to proceed with the new warning.
- Adds unit and SQL-backed end-to-end coverage for boundaries, collisions, warnings, and bulk-delete acceptance.
| File | Description |
|---|---|
| test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/Search/UriLengthSearchTests.cs | Tests prefix matches, warnings, collisions, and bulk delete. |
| test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/Search/UriLengthSearchTestFixture.cs | Creates URI boundary and collision test data. |
| test/Microsoft.Health.Fhir.Shared.Tests.E2E/Microsoft.Health.Fhir.Shared.Tests.E2E.projitems | Registers the shared E2E tests. |
| src/Microsoft.Health.Fhir.SqlServer/Features/Storage/TvpRowGeneration/Merge/UriSearchParamListRowGenerator.cs | Truncates before deduplication and summarizes oversized values. |
| src/Microsoft.Health.Fhir.SqlServer/Features/Storage/SqlServerFhirDataStore.cs | Logs URI truncation during writes and reindexing. |
| src/Microsoft.Health.Fhir.SqlServer/Features/Search/SqlServerSearchService.cs | Adds long-URI search warnings. |
| src/Microsoft.Health.Fhir.SqlServer/Features/Search/Expressions/Visitors/LongUriSearchValueFinder.cs | Finds affected URI parameters in search expressions. |
| src/Microsoft.Health.Fhir.SqlServer.UnitTests/Features/Storage/TvpRowGeneration/UriSearchParamListRowGeneratorTests.cs | Tests truncation, deduplication, and value-free logging. |
| src/Microsoft.Health.Fhir.SqlServer.UnitTests/Features/Search/SqlServerSearchServiceLongUriWarningTests.cs | Tests warning creation and duplicate suppression. |
| src/Microsoft.Health.Fhir.SqlServer.UnitTests/Features/Search/Expressions/Visitors/LongUriSearchValueFinderTests.cs | Tests boundaries and nested expression traversal. |
| src/Microsoft.Health.Fhir.Core/Resources.resx | Defines the warning message. |
| src/Microsoft.Health.Fhir.Core/Resources.Designer.cs | Exposes the warning resource accessor. |
| src/Microsoft.Health.Fhir.Core/Features/Search/LongUriSearchValueIssue.cs | Centralizes warning creation and recognition. |
| src/Microsoft.Health.Fhir.Core/Features/Operations/BulkUpdate/Handlers/CreateBulkUpdateHandler.cs | Accepts the warning while logging bulk-update risk. |
| src/Microsoft.Health.Fhir.Core/Features/Operations/BulkDelete/Handlers/CreateBulkDeleteHandler.cs | Accepts the warning while logging bulk-delete risk. |
| src/Microsoft.Health.Fhir.Core.UnitTests/Features/Search/LongUriSearchValueIssueTests.cs | Tests warning formatting and recognition. |
| src/Microsoft.Health.Fhir.Core.UnitTests/Features/Operations/BulkUpdate/CreateBulkUpdateHandlerTests.cs | Tests job creation with the warning present. |
| src/Microsoft.Health.Fhir.Core.UnitTests/Features/Operations/BulkDelete/CreateBulkDeleteHandlerTests.cs | Tests warning acceptance and invalid-parameter rejection. |
Review effort: Balanced
| OperationOutcomeIssue issue = LongUriSearchValueIssue.Create(parameterCodes, LongUriSearchValueFinder.IndexedUriMaxLength); | ||
|
|
||
| // A request can run several searches (for example $everything), so add the warning once. | ||
| if (bundleIssues.Any(i => string.Equals(i.Diagnostics, issue.Diagnostics, StringComparison.Ordinal))) |
| OperationOutcomeIssue issue = LongUriSearchValueIssue.Create(parameterCodes, LongUriSearchValueFinder.IndexedUriMaxLength); | ||
|
|
||
| // A request can run several searches (for example $everything), so add the warning once. | ||
| if (bundleIssues.Any(i => string.Equals(i.Diagnostics, issue.Diagnostics, StringComparison.Ordinal))) |
- Conditional create: when the client sends Prefer: return=OperationOutcome, add the long URI warning to the returned OperationOutcome. This also covers conditional create entries in batch bundles (response.outcome). - $bulk-delete / $bulk-update: save the warning in the orchestrator job definition at submit (optional searchWarnings field, omitted when empty) and return it as an information issue on the status read. Warnings are added after the status logic, so they don't change the returned status code. - E2E tests for conditional create, batch search and conditional create entries, and the bulk delete status. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A resource with two different values for the same URI search parameter that share the first 256 characters can't be stored in the SQL index, which compares only those characters. Previously such writes failed with HTTP 500 (duplicate key in @UriSearchParams), and an import containing one failed entirely. - Create, update, conditional and bundle writes now return 400 with an OperationOutcome naming the search parameter. In a batch, only that entry fails. - $import reports each such resource as a per-line error and imports the rest. - Reindex keeps collapsing the values into one index row, so already stored resources (for example under a new custom search parameter) still reindex. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Check for colliding URI values only on the version that becomes current. History versions aren't indexed, so they can't collide and are accepted again (for example an older version in an $import file). - $import now takes rejections from the merge results instead of a pre-filter, so history handling matches the API path. - $bulk-update counts a resource rejected by the data store as patch-failed instead of updated, adds the message to the job issues, and audits it as a failed item. - $bulk-delete with _remove-references fails the page before anything is deleted if a referencing resource is rejected, so no dangling references are left. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…165443-uri-length-warning # Conflicts: # src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Resources/Delete/DeletionServiceTests.cs
Resources stored before a URI search parameter was added can have two values of that parameter that share the first 256 indexed characters. Reindex used to fail the whole job on the SQL primary key violation. Now those values are excluded for that resource, the rest is indexed, and a warning naming the resource and search parameter is shown in the reindex job status (job still completes). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
With Prefer: return=OperationOutcome, create, update, patch, conditional update and conditional delete return the long URI warning in an OperationOutcome when the resource or the condition has a URI value that is only partially compared. Transaction entries are unchanged, because a transaction fails when an entry returns an OperationOutcome. A 412 for a condition that matched several resources now includes the warning too. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Description
SQL Server stores and compares only the first 256 characters of URI search values (
UriSearchParam.Uriisvarchar(256), with no overflow column). URIs that differ only after character 256 match each other in search, and nothing tells the caller. This PR keeps the current storage and search behavior, which is accepted as a known limitation, and makes it visible:MergeResourcesandUpdateResourceSearchParams(reindex).:above,:below, or chained) is 256 characters or longer, the search Bundle gets onewarning/informationalissue:meta.profileentries), the index can't store both. Previously the write failed with HTTP 500 (SQL error 2627, duplicate key in@UriSearchParams), and an$importfile containing one such resource failed entirely. Now:$importreports each such resource as a per-line error and imports the rest;$bulk-updatecounts a rejected resource as patch-failed, not updated, adds the message to the job issues, and audits it as a failed item;$bulk-deletewith_remove-referencesfails the page before deleting anything if a referencing resource is rejected, so no dangling references are left;UpdateResourceSearchParamshit the same SQL 2627 and the reindex job failed with the SQL error infailureDetails, leaving the new parameter unusable. Now the data store leaves those values out for that resource and search parameter only, indexes everything else, and returns a warning. The orchestrator reports it as awarningissue, so the job stays Completed and the message shows in the statusfailureDetails(the same path the existing "no search parameters to reindex" information issue uses). At most 100 warnings are listed per job, followed by a count of the rest. Searches on that parameter don't return the resource.CreateBulkDeleteHandlerandCreateBulkUpdateHandlerreturn 400 for any bundle issue raised by their validation search. They now skip the new warning, because the URI is valid, and log a warning that the filter can match more resources than intended. The warning is saved in the orchestrator job definition (new optionalsearchWarningsfield, omitted from the JSON when empty) and returned as aninformationissue on the job status read. It's added after the status logic, so it doesn't change the returned status code.Prefer: return=OperationOutcomeand the request used a URI value that is only partially compared (in the resource being written, or in the condition), the response is an OperationOutcome with the warning, keeping the status code, ETag, Last-Modified and Location headers. Conditional delete returns 200 with the OperationOutcome instead of 204. Without that Prefer header, or when there's nothing to warn about, responses are unchanged. Transaction entries never get it, because a transaction fails when any entry returns an OperationOutcome; batch entries get it inresponse.outcome. Only single-resource writes report it; import, bulk update and parallel bundles log only, because their request context can't be tied to one resource.The warning text and its match check live in Core (
LongUriSearchValueIssue), so the handlers don't depend on SQL Server code. Cosmos DB is unaffected because it has no URI length limit.Interface change:
IFhirDataStore.BulkUpdateSearchParameterIndicesAsyncnow returnsTask<IReadOnlyList<string>>(warnings) instead ofTask. Cosmos DB returns an empty list. The newReindexProcessingJobResult.Warnings/OmittedWarningCountfields are left out of the JSON when empty, so job results stay compatible with older versions in both directions.Before and after
Prefer: return=OperationOutcome, an OperationOutcome with the warning (not in transactions)$importof 1,000 resources, 30 with such URIsfailureDetails); parameter stays unusablefailureDetailsPrefer: return=OperationOutcome$bulk-delete/$bulk-updatefiltered on a 256+ character URIPrefer: return=OperationOutcome, 200 with an OperationOutcome containing the warningRelated issues
Addresses AB#210616.
Testing
Unit (SQL Server):
UriSearchParamListRowGeneratorTestscovers truncation at 255/256/257/1000 characters, de-duplication of values that differ only past 256, the log entry without the value, skipping history rows, and collision detection (two values sharing the first 256 characters, a 256-character value and a longer one starting with it, identical values, values that differ earlier, short values, different search parameters).LongUriSearchValueFinderTestscovers the boundary (255 vs. 256),:below, non-URI fields, nested/union/chained expressions and de-duplication.SqlServerSearchServiceLongUriWarningTestscovers adding the warning once per request, never including the value, and skipping when there is no request context.Unit (Core):
LongUriSearchValueIssueTests.CreateBulkDeleteHandlerTestsandCreateBulkUpdateHandlerTestscheck that the job is still created with the warning saved in its definition, and that a real invalid parameter still returns 400.GetBulkDeleteHandlerTestsandGetBulkUpdateHandlerTestscheck that the status includes the warning, and that a failed job still returns 500 with its error. The existing bulk orchestrator and processing job tests pass unchanged (150 tests).Unit (API):
FhirResultExtensionsTestschecks that additional issues are added only to an OperationOutcome response.E2E (SQL Server, new
UriLengthSearchTests): uses_profile, so the same tests run on every FHIR version:$bulk-deletewith a 256+ character_profilefilter returns 202, deletes the matching resource, and the status read includes the warning;Prefer: return=OperationOutcomereturns the warning;Prefer: return=OperationOutcomereturns 201 with the warning and a Location header, and without it returns the resource;Prefer: return=OperationOutcomestill succeeds, with no entry outcome.All 13 pass locally on Stu3, R4, R4B and R5. Unit tests cover the 412 filter (warning added once, not added to other errors) and
FhirResultwithout an information message. R4 SQL create/update/conditional/patch/delete/batch/transaction E2E classes pass (the unauthorized-transaction test also fails onmain).E2E (import, new tests in
ImportTests): a file with 3 patients, one with colliding profiles, imports 2 and reports the third in the error file; a resource whose colliding values are only in a history version imports both versions with no errors.Unit (Core):
BulkUpdateServiceTestschecks that a resource rejected by the data store is counted as patch-failed, not updated.DeletionServiceTestschecks that a rejected referencing resource stops a hard delete with_remove-referencesbefore anything is deleted (the test fails without the fix).Unit (reindex):
UriSearchParamListRowGeneratorTestschecks that only the colliding parameter's values are removed from the resource (other URI and token values stay), that the warning names the resource and parameter but not the URI, and that resources without collisions are untouched.ReindexProcessingJobTestschecks that data-store warnings end up in a completed job result, the cap of 100, and that older results without warnings still deserialize.ReindexOrchestratorJobTestschecks that warnings becomewarningissues plus an omitted-count summary, without failing the job.Manual reindex, local R4 server from
mainand from this branch: stored a Patient with two extension URIs sharing the first 256 characters, then added a customurisearch parameter on that extension and ran$reindex. Onmainthe job ended Failed ("Violation of PRIMARY KEY constraint …@UriSearchParams… error 2627"), and the new parameter stayed "not supported". On this branch the job ended Completed, 2000/2000 resources, withfailureDetails: "Resource Patient/… was not indexed for the URI search parameter 'ext-uri' because it has more than one value for it that are identical in their first 256 characters. Searches using 'ext-uri' will not return this resource." The parameter then worked for the other resources.Manual, against a local R4 server built from
mainand from this branch: a single create returned 500 before and 400 after. A batch of 10 creates (3 colliding) returned 500 for the 3 entries before and 400 after. An$importof 1,000 patients (30 colliding) failed entirely before (500, 0 imported); after, 970 were imported and the 30 were listed in the error file.The whole
Microsoft.Health.Fhir.SqlServer.UnitTestssuite passes, exceptCustomQueriesUnitTests.GivenCustomQueryClass_WithGivenWaitTime_QueryToDBWillOccurOnlyAfterWaitPeriod. That test fails only when run together with the other test in its class, passes on its own, and doesn't touch this change.FHIR Team Checklist
Semver Change (docs)
Patch