Skip to content

Commit

Permalink
[ODS-5753] Clean up message for PostgreSQL unique key constraint viol…
Browse files Browse the repository at this point in the history
…ations (#879)
  • Loading branch information
gmcelhanon authored Nov 10, 2023
1 parent 379be82 commit 760ca98
Show file tree
Hide file tree
Showing 5 changed files with 227 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,36 @@
// See the LICENSE and NOTICES files in the project root for more information.

using System;
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
using EdFi.Ods.Api.Models;
using EdFi.Ods.Common.Context;
using EdFi.Ods.Common.Security.Claims;
using NHibernate.Exceptions;
using Npgsql;

namespace EdFi.Ods.Api.ExceptionHandling.Translators.Postgres
{
public class PostgresDuplicatedKeyExceptionTranslator : IExceptionTranslator
{
private static readonly Regex _expression = new Regex(@"(?<ErrorCode>\d*): duplicate key value violates unique constraint ""(?<ConstraintName>.*?)""");
private static readonly Regex _detailExpression = new Regex(@"Key \((?<KeyColumns>.*?)\)=\((?<KeyValues>.*?)\) (?<ConstraintType>already exists).");
private const string SimpleKeyMessageFormat = "The value {0} supplied for property '{1}' of entity '{2}' is not unique.";
private const string ComposedKeyMessageFormat = "The values {0} supplied for properties '{1}' of entity '{2}' are not unique.";
private readonly IContextProvider<DataManagementResourceContext> _dataManagementResourceContextProvider;

private static readonly Regex _expression = new(@"(?<ErrorCode>\d*): duplicate key value violates unique constraint ""(?<ConstraintName>.*?)""");
private static readonly Regex _detailExpression = new(@"Key \((?<KeyColumns>.*?)\)=\((?<KeyValues>.*?)\) (?<ConstraintType>already exists).");

private const string GenericMessage = "The value(s) supplied for the resource are not unique.";

private const string SimpleKeyMessageFormat = "The value supplied for property '{0}' of entity '{1}' is not unique.";
private const string CompositeKeyMessageFormat = "The values supplied for properties '{0}' of entity '{1}' are not unique.";

private const string PrimaryKeyNameSuffix = "_pk";

public PostgresDuplicatedKeyExceptionTranslator(
IContextProvider<DataManagementResourceContext> dataManagementResourceContextProvider)
{
_dataManagementResourceContextProvider = dataManagementResourceContextProvider;
}

public bool TryTranslateMessage(Exception ex, out RESTError webServiceError)
{
Expand All @@ -33,21 +49,68 @@ public bool TryTranslateMessage(Exception ex, out RESTError webServiceError)

if (match.Success)
{
var exceptionInfo = new PostgresExceptionInfo(postgresException, _detailExpression);
var constraintName = match.Groups["ConstraintName"].ValueSpan;

string message = string.Format(exceptionInfo.IsComposedKeyConstraint
? ComposedKeyMessageFormat
: SimpleKeyMessageFormat, exceptionInfo.Values, exceptionInfo.ColumnNames, exceptionInfo.TableName);
string message = GetMessageUsingRequestContext(constraintName)
?? GetMessageUsingPostgresException();

webServiceError = new RESTError
{
Code = (int)HttpStatusCode.Conflict,
Type = "Conflict",
Message = message
};
{
Code = (int) HttpStatusCode.Conflict,
Type = "Conflict",
Message = message
};

return true;
}

string GetMessageUsingRequestContext(ReadOnlySpan<char> constraintName)
{
// Rely on PK suffix naming convention to identify PK constraint violation (which covers almost all scenarios for this violation)
if (constraintName.EndsWith(PrimaryKeyNameSuffix, StringComparison.OrdinalIgnoreCase))
{
var tableName = constraintName.Slice(0, constraintName.Length - PrimaryKeyNameSuffix.Length).ToString();

// Look for matching class in the request's targeted resource
if (_dataManagementResourceContextProvider.Get()?.Resource?
.ContainedItemTypeByName.TryGetValue(tableName, out var resourceClass) ?? false)
{
var pkPropertyNames = resourceClass.IdentifyingProperties.Select(p => p.PropertyName).ToArray();

return string.Format(
(pkPropertyNames.Length > 1)
? CompositeKeyMessageFormat
: SimpleKeyMessageFormat,
string.Join(", ", pkPropertyNames),
resourceClass.Name);
}
}

return null;
}

string GetMessageUsingPostgresException()
{
string message;
var exceptionInfo = new PostgresExceptionInfo(postgresException, _detailExpression);

// Column names will only be available form Postgres if a special argument is added to the connection string
if (exceptionInfo.ColumnNames.Length > 0 && exceptionInfo.ColumnNames != PostgresExceptionInfo.UnknownValue)
{
message = string.Format(
exceptionInfo.IsComposedKeyConstraint
? CompositeKeyMessageFormat
: SimpleKeyMessageFormat,
exceptionInfo.ColumnNames,
exceptionInfo.TableName);
}
else
{
message = GenericMessage;
}

return message;
}
}

return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public enum PostgresExceptionConstraintType

public class PostgresExceptionInfo
{
private const string UnknownValue = "unknown";
public const string UnknownValue = "unknown";

public string TableName { get; }
public string ColumnNames { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public void EnsurePersonMapsInitialized_ShouldInitializeCache_WhenNotAlreadyInit
};

A.CallTo(() => fakePersonIdentifiersProvider.GetAllPersonIdentifiersAsync(personType))
.Returns(TaskHelpers.FromResultWithDelay<IEnumerable<PersonIdentifiersValueMap>>(personIdentifiers, 25));
.Returns(TaskHelpers.FromResultWithDelay<IEnumerable<PersonIdentifiersValueMap>>(personIdentifiers, 100));

var initializer = new PersonMapCacheInitializer(
fakePersonIdentifiersProvider,
Expand Down
Loading

0 comments on commit 760ca98

Please sign in to comment.