Skip to content
Open
42 changes: 41 additions & 1 deletion src/Core/Configurations/RuntimeConfigValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -910,6 +910,9 @@ public void ValidateEntitiesDoNotGenerateDuplicateQueriesOrMutation(DatabaseType
{
HashSet<string> graphQLOperationNames = new();

// Tracks which entity registered each operation name, used only for building conflict error messages.
Dictionary<string, string> operationOwner = new();

foreach ((string entityName, Entity entity) in entityCollection)
{
if (!entity.GraphQL.Enabled)
Expand All @@ -918,6 +921,7 @@ public void ValidateEntitiesDoNotGenerateDuplicateQueriesOrMutation(DatabaseType
}

bool containsDuplicateOperationNames = false;
string conflictingEntityName = string.Empty;
if (entity.Source.Type is EntitySourceType.StoredProcedure)
{
// For Stored Procedures a single query/mutation is generated.
Expand All @@ -926,6 +930,11 @@ public void ValidateEntitiesDoNotGenerateDuplicateQueriesOrMutation(DatabaseType
if (!graphQLOperationNames.Add(storedProcedureQueryName))
{
containsDuplicateOperationNames = true;
conflictingEntityName = operationOwner.GetValueOrDefault(storedProcedureQueryName, string.Empty);
}
else
{
operationOwner[storedProcedureQueryName] = entityName;
}
}
else
Expand All @@ -952,13 +961,44 @@ public void ValidateEntitiesDoNotGenerateDuplicateQueriesOrMutation(DatabaseType
|| ((databaseType is DatabaseType.CosmosDB_NoSQL) && !graphQLOperationNames.Add(patchMutationName)))
{
containsDuplicateOperationNames = true;
conflictingEntityName =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This lookup reports only the first conflicting owner, but one entity can conflict with different prior entities on different generated names.

For example, if A uses Alpha/Shared, B uses Beta/Betas, and C uses Beta/Shared, C conflicts with B through its singular-derived operations and with A through its plural list query. The current null-coalescing chain reports only B and omits A, so the user may fix the reported conflict only to encounter another startup failure for the same entity.

Could we collect all distinct owners of the conflicting generated names and include every conflicting entity in the diagnostic instead?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think the first conflicting is enough. The developers can walk through all errors one by one, until they succeed. It would be a lot of heavy lifting to check for all conflicting errors.

operationOwner.GetValueOrDefault(pkQueryName) ??
operationOwner.GetValueOrDefault(listQueryName) ??
operationOwner.GetValueOrDefault(createMutationName) ??
operationOwner.GetValueOrDefault(updateMutationName) ??
operationOwner.GetValueOrDefault(deleteMutationName) ??
operationOwner.GetValueOrDefault(patchMutationName) ??
string.Empty;
}
else
{
operationOwner[pkQueryName] = entityName;
operationOwner[listQueryName] = entityName;
operationOwner[createMutationName] = entityName;
operationOwner[updateMutationName] = entityName;
operationOwner[deleteMutationName] = entityName;
if (databaseType is DatabaseType.CosmosDB_NoSQL)
{
operationOwner[patchMutationName] = entityName;
}
}
}
Comment on lines +973 to 985

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

That should not be possible since the if statement that is used previously to this already checks all the possible operationOwners with the name

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Validate only mode can lose operation ownership after an earlier conflict. The || chain mutates graphQLOperationNames incrementally, but operationOwner is populated only in the else block after every Add succeeds.

If an earlier Add succeeds and a later Add fails, the successfully added operation remains in graphQLOperationNames without an owner. Because validate-only mode continues processing, a later entity can collide with that operation and the resulting message omits the actual conflicting entity.

You can repro this with First (Alpha/Shared), Second (Beta/Shared), and Third (Beta/Thirds). Second successfully adds beta_by_pk before failing on Shared, but beta_by_pk is never assigned an owner. Third then conflicts on beta_by_pk, and its recorded error lists only Third instead of identifying Second.

I think we should record ownership immediately for each successful Add, or possibly use the operation-to-owner dictionary as the authoritative duplicate check.

A validate-only regression test should cover this sequence.


if (containsDuplicateOperationNames)
{
string entitiesStr = string.IsNullOrEmpty(conflictingEntityName)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The new message can report non-conflicting names as shared. entityNamesStr always contains the current entity's singular and plural names, but the message labels both values as names generated by both entities regardless of which operation actually collided.

Isn't this incorrect for singular-only and plural-only conflicts, where the other configured name can be different? Likewise for stored-procedure/table conflicts, where the generated operation fields can collide even though the entities singular and plural type names differ.

I'm thinking we should probably track and display only the generated names that actually conflict, and if the implementation can not retain that information, this section should be removed or reworded so it does not claim both entities generate both displayed names.

? $" {entityName}"
: $" {conflictingEntityName}{Environment.NewLine} {entityName}";

string entityNamesStr = $" {GraphQLNaming.GetDefinedSingularName(entityName, entity)}{Environment.NewLine} {GraphQLNaming.GetDefinedPluralName(entityName, entity)}";

string message = $"{Environment.NewLine}GraphQL naming conflict detected."
+ $"{Environment.NewLine}{Environment.NewLine}Entities:{Environment.NewLine}{entitiesStr}"
+ $"{Environment.NewLine}{Environment.NewLine}Both entities generate the following GraphQL names:{Environment.NewLine}{entityNamesStr}"
+ $"{Environment.NewLine}{Environment.NewLine}Configure distinct GraphQL singular and plural names for one of the entities to resolve this conflict.";
Comment thread
RubenCerna2079 marked this conversation as resolved.

HandleOrRecordException(new DataApiBuilderException(
message: $"Entity {entityName} generates queries/mutation that already exist",
message: message,
statusCode: HttpStatusCode.ServiceUnavailable,
subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError));
}
Expand Down
8 changes: 6 additions & 2 deletions src/Service.Tests/Configuration/ConfigurationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6292,8 +6292,12 @@ public async Task TestAutoentitiesGeneratedWithSpacesInObjectName(string tableNa
[TestCategory(TestCategory.MSSQL)]
[DataTestMethod]
[DataRow("dbo_publishers", "uniqueSingularPublisher", "uniquePluralPublishers", "/unique/publisher", "Entity 'dbo_publishers' conflicts in autoentity pattern 'PublisherAutoEntity'. Use --patterns.exclude to skip it.", DisplayName = "Autoentities fail due to entity name")]
[DataRow("UniquePublisher", "dbo_publishers", "uniquePluralPublishers", "/unique/publisher", "Entity dbo_publishers generates queries/mutation that already exist", DisplayName = "Autoentities fail due to graphql singular type")]
[DataRow("UniquePublisher", "uniqueSingularPublisher", "dbo_publishers", "/unique/publisher", "Entity dbo_publishers generates queries/mutation that already exist", DisplayName = "Autoentities fail due to graphql plural type")]
[DataRow("UniquePublisher", "dbo_publishers", "uniquePluralPublishers", "/unique/publisher",
"\r\nGraphQL naming conflict detected.\r\n\r\nEntities:\r\n UniquePublisher\r\n dbo_publishers\r\n\r\nBoth entities generate the following GraphQL names:\r\n dbo_publishers\r\n dbo_publishers\r\n\r\nConfigure distinct GraphQL singular and plural names for one of the entities to resolve this conflict.",
DisplayName = "Autoentities fail due to graphql singular type")]
[DataRow("UniquePublisher", "uniqueSingularPublisher", "dbo_publishers", "/unique/publisher",
"\r\nGraphQL naming conflict detected.\r\n\r\nEntities:\r\n UniquePublisher\r\n dbo_publishers\r\n\r\nBoth entities generate the following GraphQL names:\r\n dbo_publishers\r\n dbo_publishers\r\n\r\nConfigure distinct GraphQL singular and plural names for one of the entities to resolve this conflict.",
DisplayName = "Autoentities fail due to graphql plural type")]
[DataRow("UniquePublisher", "uniqueSingularPublisher", "uniquePluralPublishers", "/dbo_publishers", "The rest path: dbo_publishers specified for entity: dbo_publishers is already used by another entity.", DisplayName = "Autoentities fail due to rest path")]
public async Task ValidateAutoentityGenerationConflicts(string entityName, string singular, string plural, string path, string exceptionMessage)
{
Expand Down
64 changes: 55 additions & 9 deletions src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1259,7 +1259,7 @@ public void ValidateEntitiesWithGraphQLExposedGenerateDuplicateQueries(DatabaseT
{ "book", book },
{ "Book", bookWithUpperCase }
};
ValidateExceptionForDuplicateQueriesDueToEntityDefinitions(entityCollection, "Book", databaseType);
ValidateExceptionForDuplicateQueriesDueToEntityDefinitions(entityCollection, "Book", databaseType, "book");
}

/// <summary>
Expand Down Expand Up @@ -1302,7 +1302,7 @@ public void ValidateStoredProcedureAndTableGeneratedDuplicateQueries(DatabaseTyp
{ "executeBook", bookTable },
{ "Book_by_pk", bookByPkStoredProcedure }
};
ValidateExceptionForDuplicateQueriesDueToEntityDefinitions(entityCollection, "executeBook", databaseType);
ValidateExceptionForDuplicateQueriesDueToEntityDefinitions(entityCollection, "executeBook", databaseType, "Book_by_pk");
}

/// <summary>
Expand Down Expand Up @@ -1346,7 +1346,7 @@ public void ValidateStoredProcedureAndTableGeneratedDuplicateMutation(DatabaseTy
{ "ExecuteBooks", bookTable },
{ "AddBook", addBookStoredProcedure }
};
ValidateExceptionForDuplicateQueriesDueToEntityDefinitions(entityCollection, "ExecuteBooks", databaseType);
ValidateExceptionForDuplicateQueriesDueToEntityDefinitions(entityCollection, "ExecuteBooks", databaseType, "AddBook");
}

/// <summary>
Expand Down Expand Up @@ -1384,7 +1384,7 @@ public void ValidateEntitiesWithNameCollisionInGraphQLTypeGenerateDuplicateQueri
{ "book", book },
{ "book_alt", book_alt }
};
ValidateExceptionForDuplicateQueriesDueToEntityDefinitions(entityCollection, "book_alt", databaseType);
ValidateExceptionForDuplicateQueriesDueToEntityDefinitions(entityCollection, "book_alt", databaseType, "book");
}

/// <summary>
Expand Down Expand Up @@ -1427,7 +1427,7 @@ public void ValidateEntitiesWithCollisionsInSingularPluralNamesGenerateDuplicate
{ "book", book },
{ "book_alt", book_alt }
};
ValidateExceptionForDuplicateQueriesDueToEntityDefinitions(entityCollection, "book_alt", databaseType);
ValidateExceptionForDuplicateQueriesDueToEntityDefinitions(entityCollection, "book_alt", databaseType, "book");
}

/// <summary>
Expand Down Expand Up @@ -1465,7 +1465,45 @@ public void ValidateEntitiesWithNameCollisionInSingularPluralTypeGeneratesDuplic

entityCollection.Add("book_alt", book_alt);
entityCollection.Add("book", book);
ValidateExceptionForDuplicateQueriesDueToEntityDefinitions(entityCollection, "book_alt", databaseType);
ValidateExceptionForDuplicateQueriesDueToEntityDefinitions(entityCollection, "book_alt", databaseType, "book");
}

/// <summary>
/// Validates that a detailed error is thrown when autoentities includes objects whose names
/// differ only by singular/plural form (e.g. dbo.Category and dbo.Categories), causing
/// DAB to generate conflicting GraphQL type and operation names.
///
/// "dbo_Category" entity → singular: Category, plural: Categories
/// "dbo_Categories" entity → singular: Category, plural: Categories (after pluralization)
///
/// Both entities generate the same pk query, list query, and mutation names.
/// </summary>
[TestMethod]
[DataRow(DatabaseType.MSSQL)] // Relational Database
[DataRow(DatabaseType.CosmosDB_NoSQL)] // Non Relational Database
public void ValidateAutoEntitiesWithSingularPluralNameCollisionGenerateDuplicateQueries(DatabaseType databaseType)
{
// Entity Name: dbo_Category
// Singular: Category (from entity name processed by autoentities)
// Plural: Categories (pluralized from singular)
Entity categoryEntity = GraphQLTestHelpers.GenerateEntityWithSingularPlural("Category", "Categories");

// Entity Name: dbo_Categories
// Singular: Category (after singularization by autoentities)
// Plural: Categories
Entity categoriesEntity = GraphQLTestHelpers.GenerateEntityWithSingularPlural("Category", "Categories");

SortedDictionary<string, Entity> entityCollection = new()
{
{ "dbo_Categories", categoriesEntity },
{ "dbo_Category", categoryEntity }
};

ValidateExceptionForDuplicateQueriesDueToEntityDefinitions(
entityCollection,
"dbo_Category",
databaseType,
conflictingEntityName: "dbo_Categories");
}

/// <summary>
Expand Down Expand Up @@ -1612,14 +1650,22 @@ public void TestGlobalRouteValidation(string graphQLConfiguredPath, string restC
/// queries with the same name.
/// </summary>
/// <param name="entityCollection">Entity definitions</param>
/// <param name="entityName">Entity name to construct the expected exception message</param>
private static void ValidateExceptionForDuplicateQueriesDueToEntityDefinitions(SortedDictionary<string, Entity> entityCollection, string entityName, DatabaseType databaseType)
/// <param name="entityName">The entity name expected to appear in the conflict message as the conflicting entity.</param>
/// <param name="databaseType">Database type used during validation.</param>
/// <param name="conflictingEntityName">The other entity name expected to appear in the conflict message.</param>
private static void ValidateExceptionForDuplicateQueriesDueToEntityDefinitions(
SortedDictionary<string, Entity> entityCollection,
string entityName,
DatabaseType databaseType,
string conflictingEntityName)
{
RuntimeConfigValidator configValidator = InitializeRuntimeConfigValidator();
DataApiBuilderException dabException = Assert.ThrowsException<DataApiBuilderException>(
action: () => configValidator.ValidateEntitiesDoNotGenerateDuplicateQueriesOrMutation(databaseType, new(entityCollection)));

Assert.AreEqual(expected: $"Entity {entityName} generates queries/mutation that already exist", actual: dabException.Message);
StringAssert.Contains(dabException.Message, "GraphQL naming conflict detected.");
StringAssert.Contains(dabException.Message, entityName);
StringAssert.Contains(dabException.Message, conflictingEntityName);
Assert.AreEqual(expected: HttpStatusCode.ServiceUnavailable, actual: dabException.StatusCode);
Assert.AreEqual(expected: DataApiBuilderException.SubStatusCodes.ConfigValidationError, actual: dabException.SubStatusCode);
}
Expand Down