Skip to content

Commit

Permalink
[ODS-6420] Edge case: strange error result trying to delete an Educat…
Browse files Browse the repository at this point in the history
…ionOrganizationCategoryDescriptor (#1116)
  • Loading branch information
simpat-jesus authored Aug 8, 2024
1 parent 41208f5 commit 66a9726
Show file tree
Hide file tree
Showing 5 changed files with 360 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,11 @@

using System;
using System.Linq;
using System.Text.RegularExpressions;
using EdFi.Common.Configuration;
using EdFi.Common.Extensions;
using EdFi.Ods.Common.Context;
using EdFi.Ods.Common.Exceptions;
using EdFi.Ods.Common.Models;
using EdFi.Ods.Common.Models.Domain;
using EdFi.Ods.Common.Security.Claims;
using NHibernate.Exceptions;
using Npgsql;

Expand Down Expand Up @@ -47,7 +44,7 @@ public bool TryTranslate(Exception ex, out IEdFiProblemDetails problemDetails)
{
// Iterate the incoming associations looking for the offending constraint
var association = entity?.IncomingAssociations.SingleOrDefault(
a =>
a =>
a.Association.ConstraintByDatabaseEngine[DatabaseEngine.Postgres.Value]
.EqualsIgnoreCase(postgresException.ConstraintName));

Expand All @@ -63,7 +60,8 @@ public bool TryTranslate(Exception ex, out IEdFiProblemDetails problemDetails)
// NOTE: FK violations won't happen in the ODS for "update" because where key updates are allowed, cascade updates are applied.
// So this scenario will only happen with deletes where there are child aggregate/resources that must be removed first.
// In this case, the PostgreSQL exception identifies the dependent table (no translation is necesssary)
problemDetails = new DependentResourceItemExistsException(entity.Name);

problemDetails = new DependentResourceItemExistsException(entity);

return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,23 @@

using System;
using System.Text.RegularExpressions;
using Microsoft.Data.SqlClient;
using EdFi.Common.Extensions;
using EdFi.Ods.Common.Exceptions;
using EdFi.Ods.Common.Models;
using EdFi.Ods.Common.Models.Domain;
using Microsoft.Data.SqlClient;
using NHibernate.Exceptions;

namespace EdFi.Ods.Api.ExceptionHandling.Translators.SqlServer
{
public class SqlServerConstraintExceptionTranslator : IProblemDetailsExceptionTranslator
{
private readonly IDomainModelProvider _domainModelProvider;

public SqlServerConstraintExceptionTranslator(IDomainModelProvider domainModelProvider)
{
_domainModelProvider = domainModelProvider;
}

/* Sample errors:
* Delete fails, single column child reference
Expand All @@ -39,9 +47,9 @@ public class SqlServerConstraintExceptionTranslator : IProblemDetailsExceptionTr
*/

private static readonly Regex _expression = new(
@"^The (?<StatementType>INSERT|UPDATE|DELETE) statement conflicted with the (?<ConstraintType>FOREIGN KEY|REFERENCE) constraint ""(?<ConstraintName>\w+)"".*?table ""[a-z]+\.(?<TableName>\w+)""(?:, column '(?<ColumnName>\w+)')?");
@"^The (?<StatementType>INSERT|UPDATE|DELETE) statement conflicted with the (?<ConstraintType>FOREIGN KEY|REFERENCE) constraint ""(?<ConstraintName>\w+)"".*?table ""(?<SchemaName>\w+)\.(?<TableName>\w+)""(?:, column '(?<ColumnName>\w+)')?");

// ^The (?<Statement>INSERT|UPDATE|DELETE) statement conflicted with the (?<ConstraintType>FOREIGN KEY|REFERENCE) constraint "(?<ConstraintName>\w+)".*?table "[a-z]+\.(?<TableName>\w+)".*?(?: column '(?<ColumnName>\w+)')?
// ^The (?<Statement>INSERT|UPDATE|DELETE) statement conflicted with the (?<ConstraintType>FOREIGN KEY|REFERENCE) constraint "(?<ConstraintName>\w+)".*?table "(?<SchemaName>\w+)\.(?<TableName>\w+)".*?(?: column '(?<ColumnName>\w+)')?
public bool TryTranslate(Exception ex, out IEdFiProblemDetails problemDetails)
{
var exception = ex is GenericADOException
Expand All @@ -57,6 +65,7 @@ public bool TryTranslate(Exception ex, out IEdFiProblemDetails problemDetails)
{
string statementType = match.Groups["StatementType"].Value;
string constraintType = match.Groups["ConstraintType"].Value;
string schemaName = match.Groups["SchemaName"].Value;
string tableName = match.Groups["TableName"].Value;

switch (statementType)
Expand All @@ -71,13 +80,18 @@ public bool TryTranslate(Exception ex, out IEdFiProblemDetails problemDetails)
}

break;

case "DELETE":

if (constraintType == "REFERENCE")
{
problemDetails = new DependentResourceItemExistsException(tableName);
return true;
if (_domainModelProvider.GetDomainModel()
.EntityByFullName.TryGetValue(new FullName(schemaName, tableName), out var entity))
{
problemDetails = new DependentResourceItemExistsException(entity);

return true;
}
}

break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
// See the LICENSE and NOTICES files in the project root for more information.

using System.Collections.Generic;
using System.Linq;
using EdFi.Ods.Common.Models.Domain;

namespace EdFi.Ods.Common.Exceptions;

Expand All @@ -14,13 +16,35 @@ public class DependentResourceItemExistsException : ConflictException
private const string TitleText = "Dependent Item Exists";

private const string DefaultDetail = "The requested action cannot be performed because this item is referenced by another item.";
private const string DefaultDetailFormat = "The requested action cannot be performed because this item is referenced by an existing '{0}' item.";
private const string NonRootDetailFormat = "The requested action cannot be performed because this item is referenced by an existing '{0}' item.";
private const string AbstractRootDetailFormat = "The requested action cannot be performed because this item is referenced by an existing '{0}' item contained within a resource item of one of the following types: {1}.";
private const string ConcreteRootDetailFormat = "The requested action cannot be performed because this item is referenced by an existing '{0}' item contained within an '{1}' resource item.";

public DependentResourceItemExistsException()
: base(DefaultDetail) { }

public DependentResourceItemExistsException(string resourceName)
: base(string.Format(DefaultDetailFormat, resourceName)) { }
public DependentResourceItemExistsException(Entity resourceEntity)
: base(GetDetail(resourceEntity)) { }

public static string GetDetail(Entity resourceEntity)
{
if (resourceEntity.Parent is null)
{
return string.Format(NonRootDetailFormat, resourceEntity.Name);
}

if (resourceEntity.Parent.IsAbstract)
{
var derivedEntities = resourceEntity.Parent.DerivedEntities.Select(x => x.Name);
return string.Format(AbstractRootDetailFormat, resourceEntity.Name, string.Join(", ", derivedEntities));

}
else
{
return string.Format(ConcreteRootDetailFormat, resourceEntity.Name, resourceEntity.Parent.Name);

}
}

// ---------------------------
// Boilerplate for overrides
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@
using EdFi.Ods.Api.ExceptionHandling;
using EdFi.Ods.Api.ExceptionHandling.Translators;
using EdFi.Ods.Api.ExceptionHandling.Translators.SqlServer;
using EdFi.Ods.Api.Models;
using EdFi.Ods.Api.Providers;
using EdFi.Ods.Common.Exceptions;
using EdFi.Ods.Common.Models;
using EdFi.Ods.Tests._Builders;
using EdFi.TestFixture;
using FakeItEasy;
using NUnit.Framework;
using Test.Common;

namespace EdFi.Ods.Tests.EdFi.Ods.Common.ExceptionHandling
{
Expand All @@ -34,7 +35,7 @@ protected override void Arrange()

protected override void Act()
{
var translator = new SqlServerConstraintExceptionTranslator();
var translator = new SqlServerConstraintExceptionTranslator(Stub<IDomainModelProvider>());
translator.TryTranslate(_suppliedInsertException, out _actualError);
}

Expand Down Expand Up @@ -68,7 +69,7 @@ protected override void Arrange()

protected override void Act()
{
var translator = new SqlServerConstraintExceptionTranslator();
var translator = new SqlServerConstraintExceptionTranslator(Stub<IDomainModelProvider>());
translator.TryTranslate(_suppliedUpdateException, out _actualError);
}

Expand All @@ -93,17 +94,20 @@ public class When_a_delete_conflicts_with_a_reference_constraint_with_multiple_c
{
private Exception _suppliedUpdateException;
private IEdFiProblemDetails _actualError;
private IDomainModelProvider _domainModelProvider;

protected override void Arrange()
{
_domainModelProvider = DomainModelDefinitionsProviderHelper.DomainModelProvider;

_suppliedUpdateException = NHibernateExceptionBuilder.CreateException(
"could not delete: [something-a-rather][SQL: SQL not available]",
$"The DELETE statement conflicted with the REFERENCE constraint \"FK_DisciplineAction_DisciplineIncident_SchoolId\". The conflict occurred in database \"EdFi_Ods\", table \"edfi.DisciplineAction\".{Environment.NewLine}The statement has been terminated.");
}

protected override void Act()
{
var translator = new SqlServerConstraintExceptionTranslator();
var translator = new SqlServerConstraintExceptionTranslator(_domainModelProvider);
translator.TryTranslate(_suppliedUpdateException, out _actualError);
}

Expand All @@ -129,17 +133,20 @@ public class When_a_delete_conflicts_with_a_reference_constraint_with_a_single_c
{
private Exception _suppliedUpdateException;
private IEdFiProblemDetails _actualError;
private IDomainModelProvider _domainModelProvider;

protected override void Arrange()
{
_domainModelProvider = DomainModelDefinitionsProviderHelper.DomainModelProvider;

_suppliedUpdateException = NHibernateExceptionBuilder.CreateException(
"could not delete: [something-a-rather][SQL: SQL not available]",
$"The DELETE statement conflicted with the REFERENCE constraint \"FK_CourseTranscript_CourseAttemptResultType_CourseAttemptResultTypeId\". The conflict occurred in database \"EdFi_Ods\", table \"edfi.CourseTranscript\", column 'CourseAttemptResultTypeId'.{Environment.NewLine}The statement has been terminated.");
}

protected override void Act()
{
var translator = new SqlServerConstraintExceptionTranslator();
var translator = new SqlServerConstraintExceptionTranslator(_domainModelProvider);
translator.TryTranslate(_suppliedUpdateException, out _actualError);
}

Expand Down
Loading

0 comments on commit 66a9726

Please sign in to comment.