diff --git a/config-generators/mysql-commands.txt b/config-generators/mysql-commands.txt
index d322c8722a..cd2f1c58d8 100644
--- a/config-generators/mysql-commands.txt
+++ b/config-generators/mysql-commands.txt
@@ -2,6 +2,7 @@ init --config "dab-config.MySql.json" --database-type mysql --connection-string
add Publisher --config "dab-config.MySql.json" --source publishers --permissions "anonymous:read"
add Stock --config "dab-config.MySql.json" --source stocks --permissions "anonymous:create,read,update,delete"
add Book --config "dab-config.MySql.json" --source books --permissions "anonymous:create,read,update,delete" --graphql "book:books"
+add BookNF --config "dab-config.MySql.json" --source books --permissions "anonymous:*" --rest true --graphql "bookNF:booksNF"
add BookWebsitePlacement --config "dab-config.MySql.json" --source book_website_placements --permissions "anonymous:read"
add Author --config "dab-config.MySql.json" --source authors --permissions "anonymous:read"
add Review --config "dab-config.MySql.json" --source reviews --permissions "anonymous:create,read,update" --rest false --graphql "review:reviews"
diff --git a/src/Core/Resolvers/MultipleCreateOrderHelper.cs b/src/Core/Resolvers/MultipleCreateOrderHelper.cs
index 2e20181d10..83c2ebd003 100644
--- a/src/Core/Resolvers/MultipleCreateOrderHelper.cs
+++ b/src/Core/Resolvers/MultipleCreateOrderHelper.cs
@@ -24,13 +24,14 @@ public class MultipleCreateOrderHelper
/// returns the referencing entity's name for the pair of (source, target) entities.
///
/// When visualized as a graphQL mutation request,
- /// Source entity refers to the top level entity
+ /// Source entity refers to the top level entity in whose configuration the relationship is defined.
/// Target entity refers to the related entity.
///
/// This method handles the logic to determine the referencing entity for relationships from (source, target) with cardinalities:
/// 1. 1:N - Target entity is the referencing entity
/// 2. N:1 - Source entity is the referencing entity
/// 3. 1:1 - Determined based on foreign key constraint/request input data.
+ /// 4. M:N - None of the source/target entity is the referencing entity. Instead, linking table acts as the referencing entity.
///
/// GraphQL request context.
/// Configured relationship name in the config file b/w source and target entity.
@@ -40,6 +41,7 @@ public class MultipleCreateOrderHelper
/// Column name/value for backing columns present in the request input for the source entity.
/// Input GraphQL value for target node (could be an object or array).
/// Nesting level of the entity in the mutation request.
+ /// true if the relationship is a Many-Many relationship.
public static string GetReferencingEntityName(
IMiddlewareContext context,
string relationshipName,
@@ -48,7 +50,8 @@ public static string GetReferencingEntityName(
ISqlMetadataProvider metadataProvider,
Dictionary columnDataInSourceBody,
IValueNode? targetNodeValue,
- int nestingLevel)
+ int nestingLevel,
+ bool isMNRelationship = false)
{
if (!metadataProvider.GetEntityNamesAndDbObjects().TryGetValue(sourceEntityName, out DatabaseObject? sourceDbObject) ||
!metadataProvider.GetEntityNamesAndDbObjects().TryGetValue(targetEntityName, out DatabaseObject? targetDbObject))
@@ -61,10 +64,39 @@ public static string GetReferencingEntityName(
subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound);
}
+ if (sourceDbObject.GetType() != typeof(DatabaseTable) || targetDbObject.GetType() != typeof(DatabaseTable))
+ {
+ throw new DataApiBuilderException(
+ message: $"Cannot execute multiple-create for relationship: {relationshipName} at level: {nestingLevel} because currently DAB supports" +
+ $"multiple-create only for entities backed by tables.",
+ statusCode: HttpStatusCode.BadRequest,
+ subStatusCode: DataApiBuilderException.SubStatusCodes.NotSupported);
+ }
+
+ DatabaseTable sourceDbTable = (DatabaseTable)sourceDbObject;
+ DatabaseTable targetDbTable = (DatabaseTable)targetDbObject;
+ if (sourceDbTable.Equals(targetDbTable))
+ {
+ // DAB does not yet support multiple-create for self referencing relationships where both the source and
+ // target entities are backed by same database table.
+ throw new DataApiBuilderException(
+ message: $"Multiple-create for relationship: {relationshipName} at level: {nestingLevel} is not supported because the " +
+ $"source entity: {sourceEntityName} and the target entity: {targetEntityName} are backed by the same database table.",
+ statusCode: HttpStatusCode.BadRequest,
+ subStatusCode: DataApiBuilderException.SubStatusCodes.NotSupported);
+ }
+
+ if (isMNRelationship)
+ {
+ // For M:N relationships, neither the source nor the target entity act as the referencing entity.
+ // Instead, the linking table act as the referencing entity.
+ return string.Empty;
+ }
+
if (TryDetermineReferencingEntityBasedOnEntityRelationshipMetadata(
sourceEntityName: sourceEntityName,
targetEntityName: targetEntityName,
- sourceDbObject: sourceDbObject,
+ sourceDbTable: sourceDbTable,
referencingEntityName: out string? referencingEntityNameBasedOnEntityMetadata))
{
return referencingEntityNameBasedOnEntityMetadata;
@@ -100,18 +132,17 @@ public static string GetReferencingEntityName(
///
/// Source entity name.
/// Target entity name.
- /// Database object for source entity.
+ /// Database table for source entity.
/// Stores the determined referencing entity name to be returned to the caller.
/// True when the referencing entity name can be determined based on the foreign key constraint defined in the database;
/// else false.
private static bool TryDetermineReferencingEntityBasedOnEntityRelationshipMetadata(
string sourceEntityName,
string targetEntityName,
- DatabaseObject sourceDbObject,
+ DatabaseTable sourceDbTable,
[NotNullWhen(true)] out string? referencingEntityName)
{
- DatabaseTable sourceDbTable = (DatabaseTable)sourceDbObject;
- SourceDefinition sourceDefinition = sourceDbObject.SourceDefinition;
+ SourceDefinition sourceDefinition = sourceDbTable.SourceDefinition;
List targetEntityForeignKeys = sourceDefinition.SourceEntityRelationshipMap[sourceEntityName].TargetEntityToFkDefinitionMap[targetEntityName];
HashSet referencingEntityNames = new();
diff --git a/src/Core/Services/MultipleMutationInputValidator.cs b/src/Core/Services/MultipleMutationInputValidator.cs
index 1811fa39bf..5723795a30 100644
--- a/src/Core/Services/MultipleMutationInputValidator.cs
+++ b/src/Core/Services/MultipleMutationInputValidator.cs
@@ -345,15 +345,7 @@ private void ProcessRelationshipField(
const string relationshipSourceIdentifier = "$";
string targetEntityName = runtimeConfig.Entities![entityName].Relationships![relationshipName].TargetEntity;
string? linkingObject = runtimeConfig.Entities![entityName].Relationships![relationshipName].LinkingObject;
- if (!string.IsNullOrWhiteSpace(linkingObject))
- {
- // The presence of a linking object indicates that an M:N relationship exists between the current entity and the target/child entity.
- // The linking table acts as a referencing table for both the source/target entities which act as
- // referenced entities. Consequently:
- // - Column values for the target entity can't be derived from insertion in the current entity.
- // - Column values for the current entity can't be derived from the insertion in the target/child entity.
- return;
- }
+ bool isMNRelationship = !string.IsNullOrWhiteSpace(linkingObject);
// Determine the referencing entity for the current relationship field input.
string referencingEntityName = MultipleCreateOrderHelper.GetReferencingEntityName(
@@ -364,7 +356,18 @@ private void ProcessRelationshipField(
metadataProvider: metadataProvider,
columnDataInSourceBody: backingColumnData,
targetNodeValue: relationshipFieldValue,
- nestingLevel: nestingLevel);
+ nestingLevel: nestingLevel,
+ isMNRelationship: isMNRelationship);
+
+ if (isMNRelationship)
+ {
+ // The presence of a linking object indicates that an M:N relationship exists between the current entity and the target/child entity.
+ // The linking table acts as a referencing table for both the source/target entities which act as
+ // referenced entities. Consequently:
+ // - Column values for the target entity can't be derived from insertion in the current entity.
+ // - Column values for the current entity can't be derived from the insertion in the target/child entity.
+ return;
+ }
// Determine the referenced entity.
string referencedEntityName = referencingEntityName.Equals(entityName) ? targetEntityName : entityName;
diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt
index e5e46779cb..678d4fb224 100644
--- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt
+++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt
@@ -766,6 +766,32 @@
}
}
},
+ {
+ BookNF: {
+ Source: {
+ Object: books,
+ Type: Table
+ },
+ GraphQL: {
+ Singular: bookNF,
+ Plural: booksNF,
+ Enabled: true
+ },
+ Rest: {
+ Enabled: true
+ },
+ Permissions: [
+ {
+ Role: anonymous,
+ Actions: [
+ {
+ Action: *
+ }
+ ]
+ }
+ ]
+ }
+ },
{
BookWebsitePlacement: {
Source: {
diff --git a/src/Service.Tests/Unittests/MultipleCreateUnitTests/MultipleCreateOrderHelperUnitTests.cs b/src/Service.Tests/Unittests/MultipleCreateUnitTests/MultipleCreateOrderHelperUnitTests.cs
index a8ae61db00..87e3882eed 100644
--- a/src/Service.Tests/Unittests/MultipleCreateUnitTests/MultipleCreateOrderHelperUnitTests.cs
+++ b/src/Service.Tests/Unittests/MultipleCreateUnitTests/MultipleCreateOrderHelperUnitTests.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT License.
using System.Collections.Generic;
+using System.Net;
using Azure.DataApiBuilder.Core.Resolvers;
using Azure.DataApiBuilder.Service.Exceptions;
using Azure.DataApiBuilder.Service.Tests.SqlTests;
@@ -374,6 +375,23 @@ public void ValidateReferencingEntityBasedOnEntityMetadata()
#endregion
+ #region Order determination test for relationships having source/target entities backed by same database table
+
+ ///
+ /// Test to validate that when multiple-create is executed for a relationship for which source and target entities are backed by the
+ /// same database table, we throw an appropriate exception because DAB currently does not support multiple-create for such relationships.
+ ///
+ [TestMethod]
+ public void TestExceptionForSelfReferencingRelationships()
+ {
+ // Identical source and target entities backed by the same database table 'books'.
+ ValidateExceptionForSelfReferencingRelationship(sourceEntityName: "Book", targetEntityName: "Book");
+
+ // Different source and target entities backed by the same database table 'books'.
+ ValidateExceptionForSelfReferencingRelationship(sourceEntityName: "Book", targetEntityName: "BookNF");
+ }
+ #endregion
+
#region Helpers
private static void ValidateReferencingEntityForRelationship(
string sourceEntityName,
@@ -395,6 +413,35 @@ private static void ValidateReferencingEntityForRelationship(
nestingLevel: 1);
Assert.AreEqual(expectedReferencingEntityName, actualReferencingEntityName);
}
+
+ ///
+ /// Helper method to validate the exception when multiple-create is executed for a self-referencing relationship where source and target
+ /// entities are backed by the same database table.
+ ///
+ /// Name of the source entity.
+ /// NAme of the target entity.
+ private static void ValidateExceptionForSelfReferencingRelationship(
+ string sourceEntityName,
+ string targetEntityName)
+ {
+ // Setup mock IMiddlewareContext.
+ IMiddlewareContext context = SetupMiddlewareContext();
+ DataApiBuilderException ex = Assert.ThrowsException(() => MultipleCreateOrderHelper.GetReferencingEntityName(
+ relationshipName: "testRelationship", // Don't need relationship name while testing determination of referencing entity using metadata.
+ context: context,
+ sourceEntityName: sourceEntityName,
+ targetEntityName: targetEntityName,
+ metadataProvider: _sqlMetadataProvider,
+ columnDataInSourceBody: new(),
+ targetNodeValue: null,
+ nestingLevel: 1));
+
+ // Assert that the exception is as expected.
+ Assert.AreEqual(HttpStatusCode.BadRequest, ex.StatusCode);
+ Assert.AreEqual(DataApiBuilderException.SubStatusCodes.NotSupported, ex.SubStatusCode);
+ Assert.AreEqual($"Multiple-create for relationship: testRelationship at level: 1 is not supported because the source entity: {sourceEntityName} and" +
+ $" the target entity: {targetEntityName} are backed by the same database table.", ex.Message);
+ }
#endregion
#region Setup
diff --git a/src/Service.Tests/dab-config.MySql.json b/src/Service.Tests/dab-config.MySql.json
index e8401fcdcb..bfa60f263c 100644
--- a/src/Service.Tests/dab-config.MySql.json
+++ b/src/Service.Tests/dab-config.MySql.json
@@ -821,6 +821,32 @@
}
}
},
+ "BookNF": {
+ "source": {
+ "object": "books",
+ "type": "table"
+ },
+ "graphql": {
+ "enabled": true,
+ "type": {
+ "singular": "bookNF",
+ "plural": "booksNF"
+ }
+ },
+ "rest": {
+ "enabled": true
+ },
+ "permissions": [
+ {
+ "role": "anonymous",
+ "actions": [
+ {
+ "action": "*"
+ }
+ ]
+ }
+ ]
+ },
"BookWebsitePlacement": {
"source": {
"object": "book_website_placements",