diff --git a/src/Core/Resolvers/DWSqlQueryBuilder.cs b/src/Core/Resolvers/DWSqlQueryBuilder.cs
index 510155fbfb..7157a13b32 100644
--- a/src/Core/Resolvers/DWSqlQueryBuilder.cs
+++ b/src/Core/Resolvers/DWSqlQueryBuilder.cs
@@ -452,12 +452,17 @@ public string Build(SqlUpsertQueryStructure structure)
// Final query to be executed for the given PUT/PATCH operation.
StringBuilder upsertQuery = new(prefixQuery);
+ // Predicates to scope the UPDATE to the record(s) identified by the PK
+ // combined with any database policy defined for the update operation.
+ string updatePredicates = JoinPredicateStrings(pkPredicates, structure.GetDbPolicyForOperation(EntityActionOperation.Update));
+
// Query to update record (if there exists one for given PK).
StringBuilder updateQuery = new(
$"IF @ROWS_TO_UPDATE = 1 " +
$"BEGIN " +
$"UPDATE {tableName} " +
- $"SET {updateOperations} ");
+ $"SET {updateOperations} " +
+ $"WHERE {updatePredicates} ");
// End the IF block.
updateQuery.Append("END ");
diff --git a/src/Service.Tests/UnitTests/DwSqlQueryBuilderUpsertTests.cs b/src/Service.Tests/UnitTests/DwSqlQueryBuilderUpsertTests.cs
new file mode 100644
index 0000000000..b13cf6e111
--- /dev/null
+++ b/src/Service.Tests/UnitTests/DwSqlQueryBuilderUpsertTests.cs
@@ -0,0 +1,173 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Data;
+using Azure.DataApiBuilder.Auth;
+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;
+using Azure.DataApiBuilder.Core.Services;
+using Azure.DataApiBuilder.Core.Services.MetadataProviders;
+using Microsoft.AspNetCore.Http;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+
+namespace Azure.DataApiBuilder.Service.Tests.UnitTests
+{
+ ///
+ /// Regression tests for the Azure Synapse Analytics (dwsql) upsert query builder.
+ /// These validate that the generated UPDATE branch of an upsert is always scoped by a
+ /// WHERE clause targeting the primary key (and any configured update database policy).
+ /// A missing WHERE clause would cause a single PUT upsert to overwrite every row in the
+ /// target table (CWE-862).
+ ///
+ [TestClass]
+ public class DwSqlQueryBuilderUpsertTests
+ {
+ private const string ENTITY_NAME = "Book";
+ private const string SCHEMA_NAME = "dbo";
+ private const string TABLE_NAME = "books";
+
+ private delegate void TryGetColumnCallback(string entity, string field, out string? column);
+
+ ///
+ /// Maps exposed field names to backing column names (identity mapping for the test entity).
+ ///
+ private static readonly Dictionary _columnMapping = new()
+ {
+ { "id", "id" },
+ { "title", "title" },
+ { "publisher_id", "publisher_id" }
+ };
+
+ ///
+ /// Verifies that the dwsql upsert builder scopes the UPDATE branch with a WHERE clause
+ /// containing the primary key predicate and the configured update database policy.
+ ///
+ [TestMethod]
+ [TestCategory(TestCategory.DWSQL)]
+ public void DwSqlUpsertUpdateBranchContainsWhereClauseScopedByPrimaryKeyAndPolicy()
+ {
+ // Arrange
+ const string updateDbPolicy = "([publisher_id] != 0)";
+
+ SqlUpsertQueryStructure structure = CreateUpsertStructure();
+
+ // Simulate an update database policy being defined for the operation.
+ structure.DbPolicyPredicatesForOperations[EntityActionOperation.Update] = updateDbPolicy;
+
+ DwSqlQueryBuilder builder = new();
+
+ // Act
+ string query = builder.Build(structure);
+
+ // Assert
+ Assert.IsTrue(query.Contains("UPDATE", StringComparison.Ordinal), $"Expected an UPDATE statement. Query: {query}");
+
+ // Isolate the UPDATE (IF @ROWS_TO_UPDATE = 1) branch so the assertion targets the UPDATE, not the INSERT.
+ int updateIndex = query.IndexOf("UPDATE", StringComparison.Ordinal);
+ int endIndex = query.IndexOf("END", updateIndex, StringComparison.Ordinal);
+ Assert.IsTrue(endIndex > updateIndex, $"Expected the UPDATE branch to be closed by END. Query: {query}");
+ string updateBranch = query.Substring(updateIndex, endIndex - updateIndex);
+
+ Assert.IsTrue(
+ updateBranch.Contains("WHERE", StringComparison.Ordinal),
+ $"The dwsql upsert UPDATE branch MUST include a WHERE clause to scope the update. Query: {query}");
+
+ Assert.IsTrue(
+ updateBranch.Contains("[id]", StringComparison.Ordinal),
+ $"The dwsql upsert UPDATE WHERE clause MUST scope by the primary key column. Query: {query}");
+
+ Assert.IsTrue(
+ updateBranch.Contains(updateDbPolicy, StringComparison.Ordinal),
+ $"The dwsql upsert UPDATE WHERE clause MUST include the update database policy. Query: {query}");
+ }
+
+ ///
+ /// Builds a minimal for the test entity using a mocked
+ /// metadata provider so no live database is required.
+ ///
+ private static SqlUpsertQueryStructure CreateUpsertStructure()
+ {
+ SourceDefinition sourceDefinition = new()
+ {
+ PrimaryKey = new() { "id" }
+ };
+ sourceDefinition.Columns.Add("id", new ColumnDefinition
+ {
+ SystemType = typeof(int),
+ DbType = DbType.Int32
+ });
+ sourceDefinition.Columns.Add("title", new ColumnDefinition
+ {
+ SystemType = typeof(string),
+ DbType = DbType.String,
+ IsNullable = true
+ });
+ sourceDefinition.Columns.Add("publisher_id", new ColumnDefinition
+ {
+ SystemType = typeof(int),
+ DbType = DbType.Int32
+ });
+
+ DatabaseTable dbTable = new(SCHEMA_NAME, TABLE_NAME)
+ {
+ TableDefinition = sourceDefinition,
+ SourceType = EntitySourceType.Table
+ };
+
+ Mock metadataProvider = new();
+ metadataProvider.Setup(x => x.EntityToDatabaseObject)
+ .Returns(new Dictionary { { ENTITY_NAME, dbTable } });
+ metadataProvider.Setup(x => x.GetSourceDefinition(ENTITY_NAME)).Returns(sourceDefinition);
+ metadataProvider.Setup(x => x.GetDatabaseType()).Returns(DatabaseType.DWSQL);
+
+ string outColumn;
+ metadataProvider.Setup(x => x.TryGetBackingColumn(It.IsAny(), It.IsAny(), out outColumn))
+ .Callback(new TryGetColumnCallback((string entity, string field, out string? column)
+ => _columnMapping.TryGetValue(field, out column)))
+ .Returns((string entity, string field, string column) => _columnMapping.ContainsKey(field));
+
+ string outExposed;
+ metadataProvider.Setup(x => x.TryGetExposedColumnName(It.IsAny(), It.IsAny(), out outExposed))
+ .Callback(new TryGetColumnCallback((string entity, string field, out string? column)
+ => _columnMapping.TryGetValue(field, out column)))
+ .Returns((string entity, string field, string column) => _columnMapping.ContainsKey(field));
+
+ // The update policy is injected directly onto the structure, so the resolver only needs
+ // to return an empty policy (no throw) during construction.
+ Mock authorizationResolver = new();
+ authorizationResolver
+ .Setup(x => x.ProcessDBPolicy(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
+ .Returns(string.Empty);
+
+ RuntimeConfigProvider runtimeConfigProvider = TestHelper.GetRuntimeConfigProvider(TestHelper.GetRuntimeConfigLoader());
+ Mock metadataProviderFactory = new();
+ GQLFilterParser gQLFilterParser = new(runtimeConfigProvider, metadataProviderFactory.Object);
+
+ DefaultHttpContext httpContext = new();
+ httpContext.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER] = "authenticated";
+
+ Dictionary mutationParams = new()
+ {
+ { "id", 1 },
+ { "title", "The Hobbit Returns to The Shire" },
+ { "publisher_id", 1234 }
+ };
+
+ return new SqlUpsertQueryStructure(
+ entityName: ENTITY_NAME,
+ sqlMetadataProvider: metadataProvider.Object,
+ authorizationResolver: authorizationResolver.Object,
+ gQLFilterParser: gQLFilterParser,
+ mutationParams: mutationParams,
+ incrementalUpdate: false,
+ httpContext: httpContext);
+ }
+ }
+}