From afa4ebb0ba7ebc4c10a3133deab6b5bd0504713c Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Tue, 14 Jul 2026 15:53:48 +0530 Subject: [PATCH 1/5] Apply PK and update-operation policy predicates to DWSQL upsert UPDATE statement Aligns the DWSQL query builder's upsert (PUT) UPDATE branch with the MsSql and PostgreSQL builders by including the primary-key predicate and the update-operation database policy in the generated WHERE clause. --- src/Core/Resolvers/DWSqlQueryBuilder.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Core/Resolvers/DWSqlQueryBuilder.cs b/src/Core/Resolvers/DWSqlQueryBuilder.cs index 510155fbfb..f2f449a1eb 100644 --- a/src/Core/Resolvers/DWSqlQueryBuilder.cs +++ b/src/Core/Resolvers/DWSqlQueryBuilder.cs @@ -441,6 +441,13 @@ public string Build(SqlUpsertQueryStructure structure) // Predicates by virtue of PK. string pkPredicates = JoinPredicateStrings(Build(structure.Predicates)); + // Predicates by virtue of PK + database policy for the update operation. + // The database policy must be applied to the UPDATE statement's WHERE clause so that + // an upsert which resolves to an update only affects rows the caller is authorized to modify. + // Omitting it would (a) apply the update to every row in the table since only the SET clause + // would remain, and (b) bypass any row-level update policy. See MsSqlQueryBuilder for parity. + string updatePredicates = JoinPredicateStrings(pkPredicates, structure.GetDbPolicyForOperation(EntityActionOperation.Update)); + string updateOperations = Build(structure.UpdateOperations, ", "); string queryToGetCountOfRecordWithPK = $"SELECT COUNT(*) as {COUNT_ROWS_WITH_GIVEN_PK} FROM {tableName} WHERE {pkPredicates}"; @@ -457,7 +464,8 @@ public string Build(SqlUpsertQueryStructure structure) $"IF @ROWS_TO_UPDATE = 1 " + $"BEGIN " + $"UPDATE {tableName} " + - $"SET {updateOperations} "); + $"SET {updateOperations} " + + $"WHERE {updatePredicates}; "); // End the IF block. updateQuery.Append("END "); From 9be17c1ebf7f109976374d72e53605ad0510cd33 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Tue, 14 Jul 2026 16:30:03 +0530 Subject: [PATCH 2/5] Add unit test for DWSQL upsert UPDATE WHERE clause predicates Verifies the DwSql upsert UPDATE branch includes the primary-key predicate and the update-operation database policy in its WHERE clause. Runs without a live database via a mocked metadata provider. --- .../UnitTests/DwSqlQueryBuilderUnitTests.cs | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 src/Service.Tests/UnitTests/DwSqlQueryBuilderUnitTests.cs diff --git a/src/Service.Tests/UnitTests/DwSqlQueryBuilderUnitTests.cs b/src/Service.Tests/UnitTests/DwSqlQueryBuilderUnitTests.cs new file mode 100644 index 0000000000..e1a0e7dde5 --- /dev/null +++ b/src/Service.Tests/UnitTests/DwSqlQueryBuilderUnitTests.cs @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using Azure.DataApiBuilder.Auth; +using Azure.DataApiBuilder.Config.DatabasePrimitives; +using Azure.DataApiBuilder.Config.ObjectModel; +using Azure.DataApiBuilder.Core.Authorization; +using Azure.DataApiBuilder.Core.Models; +using Azure.DataApiBuilder.Core.Resolvers; +using Azure.DataApiBuilder.Core.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Azure.DataApiBuilder.Service.Tests.UnitTests +{ + /// + /// Unit tests for query generation that can run without a + /// live database by mocking the metadata provider. + /// + [TestClass] + public class DwSqlQueryBuilderUnitTests + { + private const string ENTITY = "Stock"; + private const string SCHEMA = "dbo"; + private const string TABLE = "stocks"; + + /// + /// Validates that the UPDATE branch of a DwSql upsert (PUT) is constrained by a WHERE clause + /// that combines the primary-key predicate with the update-operation database policy. + /// + /// Regression guard: previously the UPDATE branch emitted only "UPDATE ... SET ..." with no + /// WHERE clause, which (a) applied the update to every row in the table and (b) dropped any + /// configured row-level update policy. This test fails against that prior behavior and passes + /// once the PK + update policy predicates are appended to the UPDATE statement. + /// + [TestMethod] + public void DwSqlUpsert_UpdateBranch_AppliesPrimaryKeyAndUpdatePolicyPredicates() + { + ISqlMetadataProvider metadataProvider = CreateStocksMetadataProviderMock(); + + // Mutation parameters for a PUT targeting an existing composite-PK row. + Dictionary mutationParams = new() + { + { "categoryid", 100 }, + { "pieceid", 99 }, + { "categoryName", "SciFi" }, + { "piecesAvailable", 4 }, + { "piecesRequired", 5 }, + }; + + DefaultHttpContext httpContext = new(); + httpContext.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER] = "database_policy_tester"; + + // The database policy processing (claims -> predicate) is exercised elsewhere. Here we return + // an empty policy from the resolver and inject the resolved predicate directly (below) so the + // test isolates the query builder's use of the predicate. + Mock authorizationResolver = new(); + authorizationResolver + .Setup(x => x.ProcessDBPolicy(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(string.Empty); + + SqlUpsertQueryStructure upsertStructure = new( + entityName: ENTITY, + sqlMetadataProvider: metadataProvider, + authorizationResolver: authorizationResolver.Object, + gQLFilterParser: null!, + mutationParams: mutationParams, + incrementalUpdate: false, + httpContext: httpContext); + + // Simulate a resolved row-level database policy for the update operation, + // e.g. config policy "@item.pieceid ne 1". + const string updatePolicyPredicate = "([pieceid] != @pol_param0)"; + upsertStructure.DbPolicyPredicatesForOperations[EntityActionOperation.Update] = updatePolicyPredicate; + + DwSqlQueryBuilder builder = new(); + string query = builder.Build(upsertStructure); + + // The UPDATE branch must be filtered by a WHERE clause. + Assert.IsTrue(query.Contains("UPDATE") && query.Contains("SET") && query.Contains("WHERE"), + $"Generated upsert query is missing a WHERE clause on the UPDATE branch:{Environment.NewLine}{query}"); + + // Primary-key columns must appear in the generated predicates. + StringAssert.Contains(query, "[categoryid]"); + StringAssert.Contains(query, "[pieceid]"); + + // The update database policy predicate must be included in the generated query. + StringAssert.Contains(query, updatePolicyPredicate); + + // The SET clause must be followed by a WHERE before the IF/END block closes, i.e. + // "UPDATE ... SET ... WHERE ..." rather than "UPDATE ... SET ... END". + int setIdx = query.IndexOf("SET", StringComparison.Ordinal); + int whereIdx = query.IndexOf("WHERE", setIdx, StringComparison.Ordinal); + int endIdx = query.IndexOf("END", setIdx, StringComparison.Ordinal); + Assert.IsTrue(whereIdx > setIdx && (endIdx == -1 || whereIdx < endIdx), + $"UPDATE branch must contain a WHERE clause before the IF/END block terminates:{Environment.NewLine}{query}"); + } + + /// + /// Builds a mocked describing a composite-primary-key + /// "stocks" table so a can be constructed without a database. + /// + private static ISqlMetadataProvider CreateStocksMetadataProviderMock() + { + SourceDefinition sourceDefinition = new(); + sourceDefinition.Columns.Add("categoryid", new ColumnDefinition { SystemType = typeof(int) }); + sourceDefinition.Columns.Add("pieceid", new ColumnDefinition { SystemType = typeof(int) }); + sourceDefinition.Columns.Add("categoryName", new ColumnDefinition { SystemType = typeof(string), IsNullable = true }); + sourceDefinition.Columns.Add("piecesAvailable", new ColumnDefinition { SystemType = typeof(int), IsNullable = true }); + sourceDefinition.Columns.Add("piecesRequired", new ColumnDefinition { SystemType = typeof(int), IsNullable = true }); + sourceDefinition.PrimaryKey.Add("categoryid"); + sourceDefinition.PrimaryKey.Add("pieceid"); + + DatabaseTable databaseTable = new(SCHEMA, TABLE) + { + SourceType = EntitySourceType.Table, + TableDefinition = sourceDefinition + }; + + Mock metadataProvider = new(); + metadataProvider.Setup(x => x.GetDatabaseType()).Returns(DatabaseType.DWSQL); + metadataProvider.Setup(x => x.EntityToDatabaseObject) + .Returns(new Dictionary { { ENTITY, databaseTable } }); + metadataProvider.Setup(x => x.GetSourceDefinition(ENTITY)).Returns(sourceDefinition); + + // Identity mapping between exposed names and backing columns. + string? backingOut; + metadataProvider + .Setup(x => x.TryGetBackingColumn(ENTITY, It.IsAny(), out backingOut)) + .Callback(new TryGetColumnCallback((string entity, string field, out string? backing) => backing = field)) + .Returns(true); + + string? exposedOut; + metadataProvider + .Setup(x => x.TryGetExposedColumnName(ENTITY, It.IsAny(), out exposedOut)) + .Callback(new TryGetColumnCallback((string entity, string field, out string? exposed) => exposed = field)) + .Returns(true); + + return metadataProvider.Object; + } + + private delegate void TryGetColumnCallback(string entity, string field, out string? result); + } +} From 110c8591c0fbd6c6402220872f4a635aee61b88f Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Tue, 14 Jul 2026 16:40:39 +0530 Subject: [PATCH 3/5] Remove unnecessary using directive in DwSql query builder unit test --- src/Service.Tests/UnitTests/DwSqlQueryBuilderUnitTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Service.Tests/UnitTests/DwSqlQueryBuilderUnitTests.cs b/src/Service.Tests/UnitTests/DwSqlQueryBuilderUnitTests.cs index e1a0e7dde5..72f8430de2 100644 --- a/src/Service.Tests/UnitTests/DwSqlQueryBuilderUnitTests.cs +++ b/src/Service.Tests/UnitTests/DwSqlQueryBuilderUnitTests.cs @@ -7,7 +7,6 @@ using Azure.DataApiBuilder.Config.DatabasePrimitives; using Azure.DataApiBuilder.Config.ObjectModel; using Azure.DataApiBuilder.Core.Authorization; -using Azure.DataApiBuilder.Core.Models; using Azure.DataApiBuilder.Core.Resolvers; using Azure.DataApiBuilder.Core.Services; using Microsoft.AspNetCore.Http; From 09c73d3e65e15355e0705f542213b80e0926a411 Mon Sep 17 00:00:00 2001 From: Souvik Ghosh Date: Tue, 14 Jul 2026 16:41:34 +0530 Subject: [PATCH 4/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/Core/Resolvers/DWSqlQueryBuilder.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Core/Resolvers/DWSqlQueryBuilder.cs b/src/Core/Resolvers/DWSqlQueryBuilder.cs index f2f449a1eb..8a96a12824 100644 --- a/src/Core/Resolvers/DWSqlQueryBuilder.cs +++ b/src/Core/Resolvers/DWSqlQueryBuilder.cs @@ -442,10 +442,8 @@ public string Build(SqlUpsertQueryStructure structure) string pkPredicates = JoinPredicateStrings(Build(structure.Predicates)); // Predicates by virtue of PK + database policy for the update operation. - // The database policy must be applied to the UPDATE statement's WHERE clause so that - // an upsert which resolves to an update only affects rows the caller is authorized to modify. - // Omitting it would (a) apply the update to every row in the table since only the SET clause - // would remain, and (b) bypass any row-level update policy. See MsSqlQueryBuilder for parity. + // PK predicates ensure the UPDATE targets only the requested row(s); the database policy predicate + // further restricts updates to rows the caller is authorized to modify. See MsSqlQueryBuilder for parity. string updatePredicates = JoinPredicateStrings(pkPredicates, structure.GetDbPolicyForOperation(EntityActionOperation.Update)); string updateOperations = Build(structure.UpdateOperations, ", "); From 8d5a2a6c07c98bea316db23c0a75957606ede0be Mon Sep 17 00:00:00 2001 From: Souvik Ghosh Date: Tue, 14 Jul 2026 16:41:42 +0530 Subject: [PATCH 5/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../UnitTests/DwSqlQueryBuilderUnitTests.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Service.Tests/UnitTests/DwSqlQueryBuilderUnitTests.cs b/src/Service.Tests/UnitTests/DwSqlQueryBuilderUnitTests.cs index 72f8430de2..f3c8642748 100644 --- a/src/Service.Tests/UnitTests/DwSqlQueryBuilderUnitTests.cs +++ b/src/Service.Tests/UnitTests/DwSqlQueryBuilderUnitTests.cs @@ -91,11 +91,12 @@ public void DwSqlUpsert_UpdateBranch_AppliesPrimaryKeyAndUpdatePolicyPredicates( // The SET clause must be followed by a WHERE before the IF/END block closes, i.e. // "UPDATE ... SET ... WHERE ..." rather than "UPDATE ... SET ... END". - int setIdx = query.IndexOf("SET", StringComparison.Ordinal); + int updateIdx = query.IndexOf("UPDATE", StringComparison.Ordinal); + int setIdx = query.IndexOf("SET", updateIdx, StringComparison.Ordinal); int whereIdx = query.IndexOf("WHERE", setIdx, StringComparison.Ordinal); - int endIdx = query.IndexOf("END", setIdx, StringComparison.Ordinal); - Assert.IsTrue(whereIdx > setIdx && (endIdx == -1 || whereIdx < endIdx), - $"UPDATE branch must contain a WHERE clause before the IF/END block terminates:{Environment.NewLine}{query}"); + int endIdx = query.IndexOf("END", updateIdx, StringComparison.Ordinal); + Assert.IsTrue(setIdx > updateIdx && whereIdx > setIdx && (endIdx == -1 || whereIdx < endIdx), + $"UPDATE branch must contain a WHERE clause after SET and before the IF/END block terminates:{Environment.NewLine}{query}"); } ///