From 21a5917cf849234ff8adcb7f6e45c7e681ef843d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 28 Oct 2026 08:51:44 +0000
Subject: [PATCH 1/2] Initial plan
From c74a6ffc607caf083f4165dc613e64dc6842f413 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 28 Oct 2026 08:57:50 +0000
Subject: [PATCH 2/2] Fix linked datasource REST mutations
Co-authored-by: souvikghosh04 <210500244+souvikghosh04@users.noreply.github.com>
---
src/Core/Resolvers/SqlMutationEngine.cs | 3 +-
.../UnitTests/SqlMutationEngineHelperTests.cs | 101 ++++++++++++++++++
2 files changed, 102 insertions(+), 2 deletions(-)
diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs
index fc6f3f1888..11caed33df 100644
--- a/src/Core/Resolvers/SqlMutationEngine.cs
+++ b/src/Core/Resolvers/SqlMutationEngine.cs
@@ -467,8 +467,7 @@ await queryExecutor.ExecuteQueryAsync(
/// IActionResult
public async Task ExecuteAsync(RestRequestContext context)
{
- // for REST API scenarios, use the default datasource
- string dataSourceName = _runtimeConfigProvider.GetConfig().DefaultDataSourceName;
+ string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(context.EntityName);
Dictionary parameters = PrepareParameters(context);
ISqlMetadataProvider sqlMetadataProvider = _sqlMetadataProviderFactory.GetMetadataProvider(dataSourceName);
diff --git a/src/Service.Tests/UnitTests/SqlMutationEngineHelperTests.cs b/src/Service.Tests/UnitTests/SqlMutationEngineHelperTests.cs
index e9daf3dfa6..7fd0857f79 100644
--- a/src/Service.Tests/UnitTests/SqlMutationEngineHelperTests.cs
+++ b/src/Service.Tests/UnitTests/SqlMutationEngineHelperTests.cs
@@ -11,8 +11,10 @@
using System.Threading.Tasks;
using System.Transactions;
using Azure.DataApiBuilder.Auth;
+using Azure.DataApiBuilder.Config;
using Azure.DataApiBuilder.Config.DatabasePrimitives;
using Azure.DataApiBuilder.Config.ObjectModel;
+using Azure.DataApiBuilder.Core.Authorization;
using Azure.DataApiBuilder.Core.Configurations;
using Azure.DataApiBuilder.Core.Models;
using Azure.DataApiBuilder.Core.Resolvers;
@@ -479,6 +481,105 @@ public async Task PerformMutationOperation_RejectsInvalidInvocation(EntityAction
}
}
+ ///
+ /// Verifies REST mutations use metadata for the entity's datasource rather than the root datasource.
+ ///
+ [TestMethod]
+ public async Task ExecuteRestMutation_UsesEntityDataSource()
+ {
+ const string rootDataSourceName = "root";
+ const string linkedDataSourceName = "linked";
+ const string entityName = "LinkedEntity";
+
+ Entity entity = new(
+ Source: new EntitySource("dbo.LinkedTable", EntitySourceType.Table, null, null),
+ GraphQL: null,
+ Fields: null,
+ Rest: null,
+ Permissions: Array.Empty(),
+ Mappings: null,
+ Relationships: null,
+ Mcp: null);
+ RuntimeConfig runtimeConfig = new(
+ Schema: string.Empty,
+ DataSource: new DataSource(DatabaseType.MSSQL, string.Empty),
+ Runtime: new RuntimeOptions(null, null, null, null),
+ Entities: new RuntimeEntities(new Dictionary { [entityName] = entity }),
+ DefaultDataSourceName: rootDataSourceName,
+ DataSourceNameToDataSource: new Dictionary
+ {
+ [rootDataSourceName] = new(DatabaseType.MSSQL, string.Empty),
+ [linkedDataSourceName] = new(DatabaseType.MSSQL, string.Empty)
+ },
+ EntityNameToDataSourceName: new Dictionary
+ {
+ [entityName] = linkedDataSourceName
+ });
+ Mock configLoader = new(null, null);
+ configLoader.Object.RuntimeConfig = runtimeConfig;
+ RuntimeConfigProvider runtimeConfigProvider = new(configLoader.Object);
+
+ Mock rootMetadataProvider = new();
+ rootMetadataProvider.Setup(x => x.GetDatabaseType()).Returns(DatabaseType.MSSQL);
+ rootMetadataProvider.SetupGet(x => x.EntityToDatabaseObject).Returns(new Dictionary());
+
+ Mock linkedMetadataProvider = new();
+ linkedMetadataProvider.Setup(x => x.GetDatabaseType()).Returns(DatabaseType.MSSQL);
+ linkedMetadataProvider.Setup(x => x.GetSourceDefinition(entityName)).Returns(new SourceDefinition());
+ linkedMetadataProvider.SetupGet(x => x.EntityToDatabaseObject)
+ .Returns(new Dictionary
+ {
+ [entityName] = new DatabaseTable("dbo", "LinkedTable")
+ });
+
+ Mock metadataProviderFactory = new();
+ metadataProviderFactory.Setup(x => x.GetMetadataProvider(rootDataSourceName)).Returns(rootMetadataProvider.Object);
+ metadataProviderFactory.Setup(x => x.GetMetadataProvider(linkedDataSourceName)).Returns(linkedMetadataProvider.Object);
+
+ Mock queryBuilder = new();
+ queryBuilder.Setup(x => x.Build(It.IsAny())).Returns("DELETE");
+ Mock queryExecutor = new();
+ queryExecutor.Setup(x => x.ExecuteQueryAsync>(
+ It.IsAny(),
+ It.IsAny>(),
+ It.IsAny?, Task>>>(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny?>()))
+ .ReturnsAsync(new Dictionary { [nameof(DbDataReader.RecordsAffected)] = 1 });
+ Mock queryManagerFactory = new();
+ queryManagerFactory.Setup(x => x.GetQueryBuilder(DatabaseType.MSSQL)).Returns(queryBuilder.Object);
+ queryManagerFactory.Setup(x => x.GetQueryExecutor(DatabaseType.MSSQL)).Returns(queryExecutor.Object);
+
+ Mock accessor = new();
+ DefaultHttpContext httpContext = new();
+ httpContext.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER] = "anonymous";
+ accessor.Setup(x => x.HttpContext).Returns(httpContext);
+ Mock authorization = new();
+ authorization.Setup(x => x.ResolveDBPolicy(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny()))
+ .Returns(ResolvedDatabasePolicy.Empty);
+
+ SqlMutationEngine engine = new(
+ queryManagerFactory.Object,
+ metadataProviderFactory.Object,
+ Mock.Of(),
+ authorization.Object,
+ null!,
+ accessor.Object,
+ runtimeConfigProvider);
+
+ IActionResult? result = await engine.ExecuteAsync(
+ new DeleteRequestContext(entityName, new DatabaseTable("dbo", "LinkedTable"), isList: false));
+
+ Assert.IsInstanceOfType(result);
+ metadataProviderFactory.Verify(x => x.GetMetadataProvider(linkedDataSourceName), Times.Once);
+ metadataProviderFactory.Verify(x => x.GetMetadataProvider(rootDataSourceName), Times.Never);
+ }
+
///
/// Verifies empty linking-table and current-entity insert results produce their distinct failure statuses.
///