Skip to content

Conversation

@LTA-Thinking
Copy link
Contributor

@LTA-Thinking LTA-Thinking commented Sep 15, 2025

Description

Adds a query parameter for PUT updates that allows changes that only update metadata fields to happen without adding a historical version.
The parameter is _meta-history
ex: PUT <fhir base>/Patient/12456?_meta-history=false

Related issues

Addresses User Story 167335

Testing

Manual testing, new and existing unit and e2e tests

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

@SergeyGaluzo
Copy link
Contributor

Are any of these meta fields searchable?

@LTA-Thinking
Copy link
Contributor Author

Yes, tags are a default search parameter in meta and other custom search parameters can be created.

@SergeyGaluzo
Copy link
Contributor

@LTA-Thinking, @brendankowitz
Planned "meta update" performs updates of resources in place without version and last updated change. This is a violation of current FHIR storage principal - append only. I think this functionality should not be supported.

@LTA-Thinking LTA-Thinking added Enhancement Enhancement on existing functionality. Azure Healthcare APIs Label denotes that the issue or PR is relevant to the FHIR service in the Azure Healthcare APIs labels Oct 13, 2025
@LTA-Thinking LTA-Thinking added this to the CY25Q4/2Wk01 milestone Oct 13, 2025
@LTA-Thinking LTA-Thinking marked this pull request as ready for review October 14, 2025 21:33
@LTA-Thinking LTA-Thinking requested a review from a team as a code owner October 14, 2025 21:33
@feordin feordin requested a review from Copilot October 16, 2025 21:25
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces the _silent-meta query parameter for PUT update operations, allowing metadata-only changes to occur without incrementing version numbers or updating the lastUpdated timestamp. This addresses scenarios where frequent metadata updates would otherwise cause unnecessary version history bloat.

Key changes:

  • Added _silent-meta query parameter that prevents version changes when only metadata fields are modified
  • Implemented deep comparison logic to detect metadata-only changes
  • Added end-to-end tests covering various scenarios of the silent metadata update feature

Reviewed Changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/Microsoft.Health.Fhir.Core/Features/KnownQueryParameterNames.cs Adds the SilentMeta constant for the new query parameter
src/Microsoft.Health.Fhir.Core/Messages/Upsert/UpsertResourceRequest.cs Adds MetaSilent property to track the silent metadata flag
src/Microsoft.Health.Fhir.Core/Features/Persistence/ResourceWrapperOperation.cs Adds MetaSilent property to resource operation wrapper
src/Microsoft.Health.Fhir.Shared.Core/Features/Resources/Upsert/UpsertResourceHandler.cs Passes the MetaSilent flag through to the data store operation
src/Microsoft.Health.Fhir.Shared.Api/Controllers/FhirController.cs Adds metaSilent parameter to the Update endpoint
src/Microsoft.Health.Fhir.Shared.Client/IFhirClient.cs Adds silentMeta parameter to update method signatures
src/Microsoft.Health.Fhir.Shared.Client/FhirClient.cs Implements silentMeta parameter handling and URI construction
src/Microsoft.Health.Fhir.SqlServer/Features/Storage/SqlServerFhirDataStore.cs Implements the core logic to detect metadata-only changes and control history creation
src/Microsoft.Health.Fhir.Core/Extensions/ElementValueExtensions.cs Adds recursive comparison logic for ElementValue instances
src/Microsoft.Health.Fhir.Core.UnitTests/Extensions/ElementValueExtensionsTests.cs Unit tests for the ElementValue comparison logic
test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/UpdateTests.cs E2E tests validating the silent metadata update behavior
test/Microsoft.Health.Fhir.Shared.Tests.Integration/Persistence/SqlServerFhirStorageTestsFixture.cs Test fixture updates to support the new functionality
src/Microsoft.Health.Fhir.SqlServer/Features/Schema/Sql/Sprocs/MergeResources.sql Documentation comment clarification
src/Microsoft.Health.Fhir.Shared.Web/appsettings.Development.json Changes log level from Debug to Information
docs/arch/adr-2510-Silent-meta.md Architecture decision record documenting the design

provenanceHeader: null,
silentMeta: true);

// Assert: Historical record should not be created (version and lastUpdated remain change)
Copy link

Copilot AI Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected spelling of 'change' to 'unchanged'.

Suggested change
// Assert: Historical record should not be created (version and lastUpdated remain change)
// Assert: Historical record should not be created (version and lastUpdated remain unchanged)

Copilot uses AI. Check for mistakes.

if (inputMeta.Equals(existingMeta))
{
return false; // Difference is in a non-meta field
Copy link

Copilot AI Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic is inverted. When inputMeta.Equals(existingMeta) returns true, it means the metadata is the same, so changes must be in non-meta fields. However, the method should return true (indicating metadata-only changes) only when the metadata differs AND everything else is the same. This condition should return true to continue checking other fields, not false.

Suggested change
return false; // Difference is in a non-meta field
return false; // No metadata changes; any difference must be in non-meta fields

Copilot uses AI. Check for mistakes.
return false;
}

// The end of the file has been reached
Copy link

Copilot AI Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says 'end of the file' but should say 'end of the resource' or 'end of the children', as this is iterating through resource children, not file content.

Suggested change
// The end of the file has been reached
// The end of the resource's children has been reached

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +9
using System.Text;
using System.Threading.Tasks;
Copy link

Copilot AI Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove unused imports: System.Collections.Generic, System.Linq, System.Text, and System.Threading.Tasks are not used in this file.

Suggested change
using System.Text;
using System.Threading.Tasks;

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +10
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
Copy link

Copilot AI Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove unused imports: System, System.Text, and System.Threading.Tasks are not used in this file.

Suggested change
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;

Copilot uses AI. Check for mistakes.
Comment on lines 425 to 427
if (serviceType == typeof(ResourceDeserializer))
{
return ResourceDeserializer as ResourceDeserializer;
Copy link

Copilot AI Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cast is redundant and will always be null. ResourceDeserializer is of type IResourceDeserializer (from line 241), but this is casting it to the concrete type ResourceDeserializer. This should either check for typeof(IResourceDeserializer) or cast to IResourceDeserializer.

Suggested change
if (serviceType == typeof(ResourceDeserializer))
{
return ResourceDeserializer as ResourceDeserializer;
if (serviceType == typeof(IResourceDeserializer))
{
return ResourceDeserializer;

Copilot uses AI. Check for mistakes.
}
"LogLevel": {
"Default": "Information",
"Microsoft.Health": "Information",
Copy link

Copilot AI Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] This appears to be an unrelated configuration change in a development settings file. Changes to logging levels should typically be in a separate PR or should be documented in the PR description if intentional.

Suggested change
"Microsoft.Health": "Information",

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@SergeyGaluzo SergeyGaluzo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use SilentMetaUpdate instead of SilentMeta

@LTA-Thinking
Copy link
Contributor Author

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@LTA-Thinking
Copy link
Contributor Author

I changed the name to Meta History as the only thing it now does is decide to store or not store a historical version.

@LTA-Thinking LTA-Thinking merged commit 8c505de into main Nov 4, 2025
50 checks passed
@LTA-Thinking LTA-Thinking deleted the personal/rojo/put-metadata branch November 4, 2025 17:26
Copy link
Member

@brendankowitz brendankowitz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: PR #5144 - Add Silent Meta Update

Overview

This PR adds a _meta-history query parameter for PUT requests that allows metadata-only changes to be made without creating a historical version of the resource. This is an optimization to reduce database bloat when only metadata (like tags) is updated.

Critical Issue - Potential PATCH Regression and Data Loss

HIGH SEVERITY: Default Value Mismatch Causes History Loss for All PATCH Operations

Location: UpsertResourceRequest.cs:18

public UpsertResourceRequest(ResourceElement resource, BundleResourceContext bundleResourceContext = null, 
    WeakETag weakETag = null, bool metaHistory = false)  // ⚠️ Default is FALSE

Location: PatchResourceHandler.cs:82

return await _mediator.Send<UpsertResourceResponse>(
    new UpsertResourceRequest(patchedResource, request.BundleResourceContext, request.WeakETag), 
    cancellationToken);  // ⚠️ metaHistory not passed, uses default FALSE

The Problem:

  1. UpsertResourceRequest constructor defaults metaHistory = false
  2. PatchResourceHandler doesn't pass the metaHistory parameter
  3. When metaHistory = false AND ChangesAreOnlyInMetadata() returns true, NO HISTORY IS KEPT

Location: SqlServerFhirDataStore.cs:304-307

else if (!resourceExt.MetaHistory && ChangesAreOnlyInMetadata(resource, existingResource))
{
    metaHistory = false;  // Prevents history creation!
}

Location: SqlServerFhirDataStore.cs:350

mergeWrappersWithVersions.Add((new MergeResourceWrapper(resource, resourceExt.KeepHistory && metaHistory, ...

Impact Scenario:

Any PATCH operation that only modifies metadata (tags, security labels, etc.) will silently skip creating a historical version, even though:

  • The user never requested this behavior
  • PATCH should always create history (standard FHIR behavior)
  • The ADR explicitly states: "This is planned to be added to PATCH...in the future" - meaning it was NOT intended for PATCH yet

Inconsistency: ResourceWrapperOperation vs UpsertResourceRequest Defaults

Class metaHistory Default
ResourceWrapperOperation true (safe)
UpsertResourceRequest false (dangerous)

This inconsistency is confusing and error-prone.


Additional Issues

2. Fragile JSON String Parsing

Location: StringExtensions.cs:154-207 (GetJsonSection)

public static string GetJsonSection(this string input, int startingIndex)

This method parses JSON by tracking brackets and quotes manually. Risks:

  • Unicode escapes not handled (\u0022 for quote)
  • Edge cases with nested objects containing "meta": in string values could cause incorrect parsing
  • Returns empty string for unmatched brackets instead of throwing (silent failures)

Location: SqlServerFhirDataStore.cs:974

var inputMeta = inputData.GetJsonSection(inputMetaStartIndex);
inputDataWithoutMeta = inputData.Replace(inputMeta, string.Empty, StringComparison.Ordinal);

Replacing the meta section content but leaving "meta":{} structure could cause comparison mismatches.

3. Inconsistent Default in FhirController vs UpsertResourceRequest

Location: FhirController.cs:228

[FromQuery(Name = KnownQueryParameterNames.MetaHistory)] bool metaHistory = true

The controller defaults to true, but:

  • UpsertResourceRequest defaults to false
  • This means explicitly calling the constructor without the parameter behaves opposite to the HTTP API

4. Unused Import

Location: StringExtensions.cs:9

using Hl7.FhirPath.Sprache;  // Not used in file

Recommendations

Critical Fix Required:

  1. Change UpsertResourceRequest.metaHistory default to true to match safe behavior and ResourceWrapperOperation
  2. Update PatchResourceHandler to explicitly pass metaHistory: true to ensure PATCH always creates history

Suggested Fix:

// UpsertResourceRequest.cs
public UpsertResourceRequest(ResourceElement resource, BundleResourceContext bundleResourceContext = null, 
    WeakETag weakETag = null, bool metaHistory = true)  // Change to TRUE
// PatchResourceHandler.cs - Be explicit
return await _mediator.Send<UpsertResourceResponse>(
    new UpsertResourceRequest(patchedResource, request.BundleResourceContext, request.WeakETag, metaHistory: true), 
    cancellationToken);

Summary

Category Rating
Code Correctness ❌ Critical bug - PATCH silently loses history
Project Conventions ⚠️ Inconsistent defaults between classes
Performance ✅ Good - Reduces unnecessary DB writes
Test Coverage ⚠️ Missing tests for PATCH with meta-only changes
Security ⚠️ Data integrity risk (audit trail loss)

Verdict: This PR contains a critical regression where PATCH operations modifying only metadata will silently lose historical versions. This should be patched immediately.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Azure Healthcare APIs Label denotes that the issue or PR is relevant to the FHIR service in the Azure Healthcare APIs Enhancement Enhancement on existing functionality.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants