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. ///