-
Notifications
You must be signed in to change notification settings - Fork 549
Improve Import Error Handling for Database Constraint Violations #5050
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Improve Import Error Handling for Database Constraint Violations #5050
Conversation
src/Microsoft.Health.Fhir.SqlServer/Features/Storage/SqlServerFhirDataStore.cs
Fixed
Show fixed
Hide fixed
src/Microsoft.Health.Fhir.SqlServer/Features/Storage/SqlServerFhirDataStore.cs
Fixed
Show fixed
Hide fixed
src/Microsoft.Health.Fhir.SqlServer/Features/Storage/SqlServerFhirDataStore.cs
Fixed
Show fixed
Hide fixed
src/Microsoft.Health.Fhir.SqlServer/Features/Storage/SqlServerFhirDataStore.cs
Fixed
Show fixed
Hide fixed
src/Microsoft.Health.Fhir.SqlServer/Features/Storage/SqlServerFhirDataStore.cs
Fixed
Show fixed
Hide fixed
src/Microsoft.Health.Fhir.SqlServer/Features/Storage/SqlServerFhirDataStore.cs
Fixed
Show fixed
Hide fixed
src/Microsoft.Health.Fhir.SqlServer/Features/Storage/SqlServerFhirDataStore.cs
Fixed
Show fixed
Hide fixed
foreach (var row in tokenSearchParamRowGenerator.GenerateRows(new[] { wrapper })) | ||
{ | ||
if (row.Code != null && row.CodeOverflow != null && | ||
Encoding.UTF8.GetByteCount(row.Code) < VLatest.TokenSearchParam.Code.Metadata.MaxLength) | ||
{ | ||
resource.ImportError = Resources.TokenSearchParamCodeOverflow; | ||
conflicts.Add(resource); | ||
hasConflict = true; | ||
break; | ||
} | ||
} |
Check notice
Code scanning / CodeQL
Missed opportunity to use Where Note
implicitly filters its target sequence
Copilot Autofix
AI 1 day ago
Copilot could not generate an autofix suggestion
Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.
src/Microsoft.Health.Fhir.SqlServer/Features/Storage/SqlServerFhirDataStore.cs
Fixed
Show fixed
Hide fixed
foreach (var row in tokenStringCompositeSearchParamListRowGenerator.GenerateRows(new[] { wrapper })) | ||
{ | ||
if (row.Code1 != null && row.CodeOverflow1 != null && | ||
Encoding.UTF8.GetByteCount(row.Code1) < VLatest.TokenStringCompositeSearchParam.Code1.Metadata.MaxLength) | ||
{ | ||
resource.ImportError = Resources.TokenStringCompositeSearchParamCodeOverflow1; | ||
conflicts.Add(resource); | ||
hasConflict = true; | ||
break; | ||
} | ||
} |
Check notice
Code scanning / CodeQL
Missed opportunity to use Where Note
implicitly filters its target sequence
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 1 day ago
The best fix is to explicitly filter the sequence using .Where(...)
to only enumerate rows matching the filtering logic. Furthermore, as the code break
s on the first match found, it is more idiomatic to use .FirstOrDefault(...)
(or .Where(...).FirstOrDefault()
) and check if the returned element is not null
. This avoids the need for a loop and a manual break, improving readability.
Specifically, replace the loop:
foreach (var row in tokenStringCompositeSearchParamListRowGenerator.GenerateRows(new[] { wrapper }))
{
if (row.Code1 != null && row.CodeOverflow1 != null &&
Encoding.UTF8.GetByteCount(row.Code1) < VLatest.TokenStringCompositeSearchParam.Code1.Metadata.MaxLength)
{
resource.ImportError = Resources.TokenStringCompositeSearchParamCodeOverflow1;
conflicts.Add(resource);
hasConflict = true;
break;
}
}
with code that directly seeks the first offending row:
var offendingRow = tokenStringCompositeSearchParamListRowGenerator
.GenerateRows(new[] { wrapper })
.FirstOrDefault(row =>
row.Code1 != null && row.CodeOverflow1 != null &&
Encoding.UTF8.GetByteCount(row.Code1) < VLatest.TokenStringCompositeSearchParam.Code1.Metadata.MaxLength);
if (offendingRow != null)
{
resource.ImportError = Resources.TokenStringCompositeSearchParamCodeOverflow1;
conflicts.Add(resource);
hasConflict = true;
}
Only the lines in the region of the original loop (lines 790-800) need changing. No new imports are needed because System.Linq is already imported. There is no need for method definitions or additional variable declarations.
-
Copy modified lines R790-R796 -
Copy modified lines R798-R800
@@ -787,16 +787,17 @@ | ||
} | ||
|
||
// Token-String Composite search param constraint validation | ||
foreach (var row in tokenStringCompositeSearchParamListRowGenerator.GenerateRows(new[] { wrapper })) | ||
var offendingRow = tokenStringCompositeSearchParamListRowGenerator | ||
.GenerateRows(new[] { wrapper }) | ||
.FirstOrDefault(row => | ||
row.Code1 != null && row.CodeOverflow1 != null && | ||
Encoding.UTF8.GetByteCount(row.Code1) < VLatest.TokenStringCompositeSearchParam.Code1.Metadata.MaxLength); | ||
|
||
if (offendingRow != null) | ||
{ | ||
if (row.Code1 != null && row.CodeOverflow1 != null && | ||
Encoding.UTF8.GetByteCount(row.Code1) < VLatest.TokenStringCompositeSearchParam.Code1.Metadata.MaxLength) | ||
{ | ||
resource.ImportError = Resources.TokenStringCompositeSearchParamCodeOverflow1; | ||
conflicts.Add(resource); | ||
hasConflict = true; | ||
break; | ||
} | ||
resource.ImportError = Resources.TokenStringCompositeSearchParamCodeOverflow1; | ||
conflicts.Add(resource); | ||
hasConflict = true; | ||
} | ||
|
||
if (hasConflict) |
foreach (var row in tokenQuantityCompositeSearchParamListRowGenerator.GenerateRows(new[] { wrapper })) | ||
{ | ||
if (row.Code1 != null && row.CodeOverflow1 != null && | ||
Encoding.UTF8.GetByteCount(row.Code1) < VLatest.TokenQuantityCompositeSearchParam.Code1.Metadata.MaxLength) | ||
{ | ||
resource.ImportError = Resources.TokenQuantityCompositeSearchParamCodeOverflow1; | ||
conflicts.Add(resource); | ||
hasConflict = true; | ||
break; | ||
} | ||
} |
Check notice
Code scanning / CodeQL
Missed opportunity to use Where Note
implicitly filters its target sequence
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 1 day ago
To fix this, refactor the loop on line 808 so it iterates only over those elements that match the current filter condition. That is, replace:
foreach (var row in tokenQuantityCompositeSearchParamListRowGenerator.GenerateRows(new[] { wrapper }))
{
if (row.Code1 != null && row.CodeOverflow1 != null &&
Encoding.UTF8.GetByteCount(row.Code1) < VLatest.TokenQuantityCompositeSearchParam.Code1.Metadata.MaxLength)
{
resource.ImportError = Resources.TokenQuantityCompositeSearchParamCodeOverflow1;
conflicts.Add(resource);
hasConflict = true;
break;
}
}
with:
foreach (var row in tokenQuantityCompositeSearchParamListRowGenerator.GenerateRows(new[] { wrapper })
.Where(row => row.Code1 != null && row.CodeOverflow1 != null &&
Encoding.UTF8.GetByteCount(row.Code1) < VLatest.TokenQuantityCompositeSearchParam.Code1.Metadata.MaxLength))
{
resource.ImportError = Resources.TokenQuantityCompositeSearchParamCodeOverflow1;
conflicts.Add(resource);
hasConflict = true;
break;
}
This pushes the filter into the enumerated sequence and eliminates unnecessary nesting. No change to imports is necessary because System.Linq
is already imported, and the pattern matches those elsewhere in the file.
-
Copy modified lines R808-R810 -
Copy modified lines R812-R815
@@ -805,16 +805,14 @@ | ||
} | ||
|
||
// Token-Quantity Composite search param constraint validation | ||
foreach (var row in tokenQuantityCompositeSearchParamListRowGenerator.GenerateRows(new[] { wrapper })) | ||
foreach (var row in tokenQuantityCompositeSearchParamListRowGenerator.GenerateRows(new[] { wrapper }) | ||
.Where(row => row.Code1 != null && row.CodeOverflow1 != null && | ||
Encoding.UTF8.GetByteCount(row.Code1) < VLatest.TokenQuantityCompositeSearchParam.Code1.Metadata.MaxLength)) | ||
{ | ||
if (row.Code1 != null && row.CodeOverflow1 != null && | ||
Encoding.UTF8.GetByteCount(row.Code1) < VLatest.TokenQuantityCompositeSearchParam.Code1.Metadata.MaxLength) | ||
{ | ||
resource.ImportError = Resources.TokenQuantityCompositeSearchParamCodeOverflow1; | ||
conflicts.Add(resource); | ||
hasConflict = true; | ||
break; | ||
} | ||
resource.ImportError = Resources.TokenQuantityCompositeSearchParamCodeOverflow1; | ||
conflicts.Add(resource); | ||
hasConflict = true; | ||
break; | ||
} | ||
|
||
if (hasConflict) |
foreach (var row in tokenNumberNumberCompositeSearchParamListRowGenerator.GenerateRows(new[] { wrapper })) | ||
{ | ||
if (row.Code1 != null && row.CodeOverflow1 != null && | ||
Encoding.UTF8.GetByteCount(row.Code1) < VLatest.TokenNumberNumberCompositeSearchParam.Code1.Metadata.MaxLength) | ||
{ | ||
resource.ImportError = Resources.TokenNumberNumberCompositeSearchParamCodeOverflow1; | ||
conflicts.Add(resource); | ||
hasConflict = true; | ||
break; | ||
} | ||
} |
Check notice
Code scanning / CodeQL
Missed opportunity to use Where Note
implicitly filters its target sequence
Copilot Autofix
AI 1 day ago
Copilot could not generate an autofix suggestion
Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.
foreach (var row in tokenDateTimeCompositeSearchParamListRowGenerator.GenerateRows(new[] { wrapper })) | ||
{ | ||
if (row.Code1 != null && row.CodeOverflow1 != null && | ||
Encoding.UTF8.GetByteCount(row.Code1) < VLatest.TokenDateTimeCompositeSearchParam.Code1.Metadata.MaxLength) | ||
{ | ||
resource.ImportError = Resources.TokenDateTimeCompositeSearchParamCodeOverflow1; | ||
conflicts.Add(resource); | ||
hasConflict = true; | ||
break; | ||
} | ||
} |
Check notice
Code scanning / CodeQL
Missed opportunity to use Where Note
implicitly filters its target sequence
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 1 day ago
To fix the problem, refactor the foreach
loop at line 844 to use .Where(...)
in LINQ, expressing the filtering explicitly. However, since the loop sets some state (resource.ImportError
, conflicts.Add(resource)
, hasConflict
) and breaks after the first match, the general idiom is to use .FirstOrDefault(...)
or .Any(...)
for such early-exit filters.
Specifically:
- Replace the
foreach
loop with a LINQ expression that finds the first row matching the condition. - If a matching row is found, execute the same block as before.
- This not only reduces nesting but also clarifies intent.
- No new imports are needed, as LINQ is already imported.
- Only the block at lines 844–854 is changed.
-
Copy modified lines R844-R848 -
Copy modified lines R850-R852
@@ -841,16 +841,15 @@ | ||
} | ||
|
||
// Token-DateTime Composite search param constraint validation | ||
foreach (var row in tokenDateTimeCompositeSearchParamListRowGenerator.GenerateRows(new[] { wrapper })) | ||
var invalidRow = tokenDateTimeCompositeSearchParamListRowGenerator | ||
.GenerateRows(new[] { wrapper }) | ||
.FirstOrDefault(row => row.Code1 != null && row.CodeOverflow1 != null && | ||
Encoding.UTF8.GetByteCount(row.Code1) < VLatest.TokenDateTimeCompositeSearchParam.Code1.Metadata.MaxLength); | ||
if (invalidRow != null) | ||
{ | ||
if (row.Code1 != null && row.CodeOverflow1 != null && | ||
Encoding.UTF8.GetByteCount(row.Code1) < VLatest.TokenDateTimeCompositeSearchParam.Code1.Metadata.MaxLength) | ||
{ | ||
resource.ImportError = Resources.TokenDateTimeCompositeSearchParamCodeOverflow1; | ||
conflicts.Add(resource); | ||
hasConflict = true; | ||
break; | ||
} | ||
resource.ImportError = Resources.TokenDateTimeCompositeSearchParamCodeOverflow1; | ||
conflicts.Add(resource); | ||
hasConflict = true; | ||
} | ||
|
||
if (hasConflict) |
foreach (var row in referenceTokenCompositeSearchParamListRowGenerator.GenerateRows(new[] { wrapper })) | ||
{ | ||
if (row.Code2 != null && row.CodeOverflow2 != null && | ||
Encoding.UTF8.GetByteCount(row.Code2) < VLatest.ReferenceTokenCompositeSearchParam.Code2.Metadata.MaxLength) | ||
{ | ||
resource.ImportError = Resources.ReferenceTokenCompositeSearchParamCodeOverflow2; | ||
conflicts.Add(resource); | ||
hasConflict = true; | ||
break; | ||
} | ||
} |
Check notice
Code scanning / CodeQL
Missed opportunity to use Where Note
implicitly filters its target sequence
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 1 day ago
To fix the problem, the iteration over referenceTokenCompositeSearchParamListRowGenerator.GenerateRows(new[] { wrapper })
should be replaced with a foreach
over the filtered/conditioned sequence using .Where(...)
, where the lambda matches the filtering condition currently present in the if
statement (row.Code2 != null && row.CodeOverflow2 != null && Encoding.UTF8.GetByteCount(row.Code2) < VLatest.ReferenceTokenCompositeSearchParam.Code2.Metadata.MaxLength
). Additionally, because the body of the if
becomes the body of the loop (now only executed for matching rows), we can remove the if
and have a flat block. No new dependencies are required, as LINQ is already imported. Only the relevant lines in the loop need changing (foreach (... in ...)
and internal use of if
). No other refactoring or functional change is required.
-
Copy modified lines R862-R865 -
Copy modified lines R867-R870
@@ -859,16 +859,15 @@ | ||
} | ||
|
||
// Reference-Token Composite search param constraint validation | ||
foreach (var row in referenceTokenCompositeSearchParamListRowGenerator.GenerateRows(new[] { wrapper })) | ||
foreach (var row in referenceTokenCompositeSearchParamListRowGenerator.GenerateRows(new[] { wrapper }) | ||
.Where(row => row.Code2 != null | ||
&& row.CodeOverflow2 != null | ||
&& Encoding.UTF8.GetByteCount(row.Code2) < VLatest.ReferenceTokenCompositeSearchParam.Code2.Metadata.MaxLength)) | ||
{ | ||
if (row.Code2 != null && row.CodeOverflow2 != null && | ||
Encoding.UTF8.GetByteCount(row.Code2) < VLatest.ReferenceTokenCompositeSearchParam.Code2.Metadata.MaxLength) | ||
{ | ||
resource.ImportError = Resources.ReferenceTokenCompositeSearchParamCodeOverflow2; | ||
conflicts.Add(resource); | ||
hasConflict = true; | ||
break; | ||
} | ||
resource.ImportError = Resources.ReferenceTokenCompositeSearchParamCodeOverflow2; | ||
conflicts.Add(resource); | ||
hasConflict = true; | ||
break; | ||
} | ||
} | ||
catch (Exception ex) |
catch (Exception ex) | ||
{ | ||
_logger.LogWarning(ex, string.Format(Resources.ExceptionWhileResourceValidation, resource.Index)); | ||
resource.ImportError = string.Format(Resources.ExceptionWhileResourceValidation, resource.Index); | ||
conflicts.Add(resource); | ||
continue; | ||
} |
Check notice
Code scanning / CodeQL
Generic catch clause Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 1 day ago
To fix the overly broad catch clause on line 874 of src/Microsoft.Health.Fhir.SqlServer/Features/Storage/SqlServerFhirDataStore.cs
, refine the exception handling to catch only exceptions that can reasonably be expected during resource validation—typically ArgumentException
, InvalidOperationException
, or other relevant exceptions thrown by row generation or resource processing. Replace the generic catch (Exception ex)
with either one or several specific catch clauses, or a multi-catch with the most appropriate types.
Edit only the block beginning at line 874:
874: catch (Exception ex)
875: {
876: _logger.LogWarning(ex, string.Format(Resources.ExceptionWhileResourceValidation, resource.Index));
877: resource.ImportError = string.Format(Resources.ExceptionWhileResourceValidation, resource.Index);
878: conflicts.Add(resource);
879: continue;
880: }
Change it to catch specific exception types (for example, ArgumentException
, InvalidOperationException
) and, if needed, add additional types if row generation is known to throw other exceptions.
No new imports are needed, as both exception types are in System
. No new definitions or methods are required.
-
Copy modified line R874 -
Copy modified lines R881-R887
@@ -871,13 +871,20 @@ | ||
} | ||
} | ||
} | ||
catch (Exception ex) | ||
catch (ArgumentException ex) | ||
{ | ||
_logger.LogWarning(ex, string.Format(Resources.ExceptionWhileResourceValidation, resource.Index)); | ||
resource.ImportError = string.Format(Resources.ExceptionWhileResourceValidation, resource.Index); | ||
conflicts.Add(resource); | ||
continue; | ||
} | ||
catch (InvalidOperationException ex) | ||
{ | ||
_logger.LogWarning(ex, string.Format(Resources.ExceptionWhileResourceValidation, resource.Index)); | ||
resource.ImportError = string.Format(Resources.ExceptionWhileResourceValidation, resource.Index); | ||
conflicts.Add(resource); | ||
continue; | ||
} | ||
|
||
if (!hasConflict) | ||
{ |
foreach (var row in tokenTokenCompositeSearchParamRowGenerator.GenerateRows(new[] { wrapper })) | ||
{ | ||
// Check if constraint for CodeOverflow1 or CodeOverflow2 is violating | ||
if ((row.CodeOverflow1 != null && Encoding.UTF8.GetByteCount(row.Code1 ?? string.Empty) < VLatest.TokenTokenCompositeSearchParam.Code1.Metadata.MaxLength) || | ||
(row.CodeOverflow2 != null && Encoding.UTF8.GetByteCount(row.Code2 ?? string.Empty) < VLatest.TokenTokenCompositeSearchParam.Code2.Metadata.MaxLength)) | ||
{ | ||
resource.ImportError = Resources.TokenTokenCompositeSearchParamCodeOverflow; | ||
conflicts.Add(resource); | ||
hasConflict = true; | ||
break; | ||
} | ||
} |
Check notice
Code scanning / CodeQL
Missed opportunity to use Where Note
implicitly filters its target sequence
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 1 day ago
The best way to fix this is to replace the entire block of code that checks each row for validity and sets hasConflict
with a more idiomatic LINQ expression. Specifically, use .Any(...)
on the generated rows with the same filter condition, and handle registration and error setting inside the conditional branch. This removes the explicit loop and clarifies the logic. Make sure the side effects (setting ImportError
, adding to conflicts
) happen only the first time the condition is met (which aligns with the current behavior). The replacement should cover lines 771–782, and you may need several lines of surrounding context for clarity. No new imports are required, as System.Linq
is already imported.
-
Copy modified lines R771-R775 -
Copy modified lines R777-R779
@@ -768,17 +768,15 @@ | ||
} | ||
|
||
// Token-Token Composite search param constraint validation | ||
foreach (var row in tokenTokenCompositeSearchParamRowGenerator.GenerateRows(new[] { wrapper })) | ||
if (tokenTokenCompositeSearchParamRowGenerator | ||
.GenerateRows(new[] { wrapper }) | ||
.Any(row => | ||
(row.CodeOverflow1 != null && Encoding.UTF8.GetByteCount(row.Code1 ?? string.Empty) < VLatest.TokenTokenCompositeSearchParam.Code1.Metadata.MaxLength) || | ||
(row.CodeOverflow2 != null && Encoding.UTF8.GetByteCount(row.Code2 ?? string.Empty) < VLatest.TokenTokenCompositeSearchParam.Code2.Metadata.MaxLength))) | ||
{ | ||
// Check if constraint for CodeOverflow1 or CodeOverflow2 is violating | ||
if ((row.CodeOverflow1 != null && Encoding.UTF8.GetByteCount(row.Code1 ?? string.Empty) < VLatest.TokenTokenCompositeSearchParam.Code1.Metadata.MaxLength) || | ||
(row.CodeOverflow2 != null && Encoding.UTF8.GetByteCount(row.Code2 ?? string.Empty) < VLatest.TokenTokenCompositeSearchParam.Code2.Metadata.MaxLength)) | ||
{ | ||
resource.ImportError = Resources.TokenTokenCompositeSearchParamCodeOverflow; | ||
conflicts.Add(resource); | ||
hasConflict = true; | ||
break; | ||
} | ||
resource.ImportError = Resources.TokenTokenCompositeSearchParamCodeOverflow; | ||
conflicts.Add(resource); | ||
hasConflict = true; | ||
} | ||
|
||
if (hasConflict) |
var referenceTokenCompositeSearchParamListRowGenerator = new ReferenceTokenCompositeSearchParamListRowGenerator(_model, new ReferenceSearchParamListRowGenerator(_model, _searchParameterTypeMap), tokenSearchParamRowGenerator, _searchParameterTypeMap); | ||
|
||
// Traverse through the resources to check which resources are causing constraint violation | ||
foreach (var resource in resources) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What kind of impact do we think this will have on $import performance?
…_proper_message_in_400_2
Description
This change introduces a pre-flight validation step within the $import data pipeline to proactively detect and report database constraint violations on a per-resource basis.
Related issues
Addresses AB# 151709
Testing
For now it just contains sample change for one constraint to get approval for the approach
FHIR Team Checklist
Semver Change (docs)
Patch|Skip|Feature|Breaking (reason)