Skip to content

Add unit tests for SearchParameter duplicate URL check - #5718

Merged
apurvabhaleMS merged 21 commits into
mainfrom
personal/abhale/duplicate-key-error
Aug 11, 2026
Merged

Add unit tests for SearchParameter duplicate URL check#5718
apurvabhaleMS merged 21 commits into
mainfrom
personal/abhale/duplicate-key-error

Conversation

@apurvabhaleMS

@apurvabhaleMS apurvabhaleMS commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Description

Adds two unit tests to SearchParameterBehaviorTests to cover the duplicate URL guard added in the companion fix:

  • GivenAnUpsertResourceRequest_WhenUrlAlreadyOwnedBySameResource_ThenNoExceptionThrown — verifies that updating a SearchParameter does not throw when the URL found in the store belongs to the same resource being updated
  • GivenAnUpsertResourceRequest_WhenUrlAlreadyOwnedByDifferentResource_ThenBadRequestThrown — verifies that a BadRequestException is thrown when a different resource already owns the target URL

Also adds a default mock for GetSearchParametersByUrlsAsync in the test constructor so existing tests continue to pass without changes.

Related issues

Addresses AB198804, AB187119

Testing

Describe how this change was tested.

FHIR Team Checklist

  • Update the title of the PR to be succinct and less than 65 characters
  • Add a milestone to the PR for the sprint that it is merged (i.e. add S47)
  • Tag the PR with the type of update: Bug, Build, Dependencies, Enhancement, New-Feature or Documentation
  • Tag the PR with Open source, Azure API for FHIR (CosmosDB or common code) or Azure Healthcare APIs (SQL or common code) to specify where this change is intended to be released.
  • Tag the PR with Schema Version backward compatible or Schema Version backward incompatible or Schema Version unchanged if this adds or updates Sql script which is/is not backward compatible with the code.
  • When changing or adding behavior, if your code modifies the system design or changes design assumptions, please create and include an ADR.
  • CI is green before merge Build Status
  • Review squash-merge requirements

Semver Change (docs)

Patch|Skip|Feature|Breaking (reason)

… SQL error 2627

ICM-833659983 / AB#198804

Root cause: TryGetValue + Remove on the shared HTTP request-context Properties
dictionary were not atomic. Under parallel bundle processing, two concurrent
threads could both read the same PendingSearchParameterStatus before either
removed it, causing pendingStatuses = [URI, URI] in MergeAsync. This produced
a duplicate row in the @searchParams TVP, violating the UNIQUE (Uri) constraint
on dbo.SearchParamList and returning SQL error 2627 / HTTP 500.

Fix: wrap TryGetValue + Remove in lock(properties) so only one thread can
claim the pending status; subsequent threads see the key already removed.

Tests added:
- Unit: SetAndClearPendingSearchParameterStatus_WhenCalledConcurrently_Only
  OneResourceReceivesStatus (SqlServerFhirDataStoreUnitTests) — deterministic,
  proves race with Parallel.For, fails before fix / passes after.
- E2E regression guard: GivenParallelTransactionBundleWithSearchParamAndPatients
  _WhenPosted_ShouldNotThrowUniqueKeyConstraint (ReindexTests) — probabilistic,
  runs in CI/EUAP.
- catch (Exception) instead of bare catch (CodeQL SA1025)
- Assert bundle response NotNull + correct entry count before checking failures
- Fail fast on reflection field-not-found instead of silent ?.SetValue
- Remove DistinctBy from E2E XML doc comment (lock-only fix was applied)
- Soften 'DETERMINISTIC' claim in unit test summary comment
…rallel coverage

Finding 1 (medium): add code comment to SetAndClearPendingSearchParameterStatus
documenting the pre-existing single-slot limitation — bundles with multiple
SearchParameter entries can lose earlier statuses due to key overwrite before
consume. Pre-existing behavior, tracked separately.

Finding 2 (medium): update E2E test XML doc to explain why Transaction was
chosen over Batch and confirm that both parallel paths are protected by the same
lock fix via the shared SetAndClearPendingSearchParameterStatus code path.

Finding 3 (low): no code change needed — the lock prevents duplicates regardless
of URL length; the 128-char boundary is a TVP schema constraint unrelated to the
concurrency fix.
…different resource

A PUT with a new unique resource ID but an existing URL violates the
1-URL-per-resource invariant. Two resources sharing a URL causes SQL 2627
errors in bundle operations: DELETEs operate by resource ID and derive
SearchParameter URLs at runtime — if two IDs share a URL, MergeAsync
receives @searchParams TVP rows [URL, URL] → UNIQUE constraint violation.

Fix: in CreateOrUpdateSearchParameterBehavior, when prevSearchParamResource
is null (brand-new resource being PUT), reject if the URL is already
registered in the SearchParameter definition manager.

Note: this fix is complementary to the ICM-833659983 lock fix (concurrent
SetAndClearPendingSearchParameterStatus race) committed earlier in this PR.
With Bug 187119 fixed, duplicate URLs cannot enter the system via PUT,
eliminating the DELETE-derived duplicate scenario. The race-condition lock
remains as defence-in-depth for the parallel-bundle scenario.
…dingHardDelete)

TryGetSearchParameter without excludePendingDelete returns ALL states including
PendingHardDelete. The previous check would block legitimate recreation of a
SearchParameter after hard-delete (the URL is still in the definition manager
as PendingHardDelete until a reindex cleans it up).

Only reject when the existing SP is in an active state (Supported, Disabled).
Allow the PUT when the existing holder is PendingDelete or PendingHardDelete.

This preserves the GivenBulkDeleteRequest_WhenSearchParametersDeleted test flow:
HardDelete -> URL enters PendingHardDelete state -> PUT recreates the same SP.
PUT was already guarded. POST (CreateAsync) had no URL-uniqueness check,
so posting a SearchParameter with a URL already owned by an active resource
silently created a second resource, accumulating duplicate-URL pairs over
repeated EUAP test runs.

Root cause of the aaaa URL errors on EUAP:
  GivenAnExistingSearchParameter_WhenUpdatingWithUrlLongerThan128 PUTs a
  SearchParameter with Url = prefix + 88 * 'a' (128 chars, always the same)
  on every run but does not clean it up. After N runs, N resources share
  URL=aaaa. Any parallel delete bundle (e.g. ReindexTests.InitializeAsync
  DeleteResourcesAsync) derives URL=aaaa for each, hitting the TVP UNIQUE
  constraint (SQL 2627).

Same guard as PUT: reject only when existing SP is in an active state
(Supported / Disabled), allow when PendingDelete / PendingHardDelete.
…ated unit test

The lock was added to prevent SQL 2627 caused by duplicate URIs in the
@searchParams TVP. The actual root cause is Bug 187119: PUT/POST allowed
creating a new SearchParameter resource with a URL already owned by a
different active resource. With that fix in place, duplicate-URL resources
can no longer accumulate, so the parallel-delete scenario that triggered
the TVP violation cannot arise.

Revert the lock and remove its unit test.
The E2E regression guard in ReindexTests remains.
…rlLongerThan128 test

Without cleanup, each test run left an active SearchParameter with URL=aaaa
(128 chars, always the same) in the database. Accumulation over repeated EUAP
runs meant N resources shared the same URL — the parallel delete bundles in
ReindexTests.InitializeAsync would then fail with SQL 2627.

Add a finally block to hard-delete the created SP so each run is self-contained.
@apurvabhaleMS apurvabhaleMS added this to the FY27\Q1\2wk\2wk03 milestone Aug 11, 2026
@apurvabhaleMS apurvabhaleMS added Enhancement-Test Enhancement on tests. Area-SQL Area related to the SQL Server data provider Azure Healthcare APIs Label denotes that the issue or PR is relevant to the FHIR service in the Azure Healthcare APIs No-PaaS-breaking-change No-ADR ADR not needed labels Aug 11, 2026
@apurvabhaleMS apurvabhaleMS changed the title Personal/abhale/duplicate key error Add unit tests for SearchParameter duplicate URL check Aug 11, 2026
@apurvabhaleMS
apurvabhaleMS marked this pull request as ready for review August 11, 2026 17:32
@apurvabhaleMS
apurvabhaleMS requested a review from a team as a code owner August 11, 2026 17:32
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (main@7e78105). Learn more about missing BASE report.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main    #5718   +/-   ##
=======================================
  Coverage        ?   77.16%           
=======================================
  Files           ?     1007           
  Lines           ?    37180           
  Branches        ?     5662           
=======================================
  Hits            ?    28691           
  Misses          ?     7120           
  Partials        ?     1369           
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@apurvabhaleMS
apurvabhaleMS merged commit 3f756fc into main Aug 11, 2026
49 of 55 checks passed
@apurvabhaleMS
apurvabhaleMS deleted the personal/abhale/duplicate-key-error branch August 11, 2026 20:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-SQL Area related to the SQL Server data provider Azure Healthcare APIs Label denotes that the issue or PR is relevant to the FHIR service in the Azure Healthcare APIs Enhancement-Test Enhancement on tests. No-ADR ADR not needed No-PaaS-breaking-change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants