-
Notifications
You must be signed in to change notification settings - Fork 348
Fix missing WHERE clause in DWSql upsert UPDATE branch #3698
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
naxing123
wants to merge
1
commit into
main
Choose a base branch
from
msrc_fix
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+179
−1
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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} " + | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you verify this manually against a DWsql instance? If yes, please mention it in the PR description. |
||
| $"WHERE {updatePredicates} "); | ||
|
|
||
| // End the IF block. | ||
| updateQuery.Append("END "); | ||
|
|
||
173 changes: 173 additions & 0 deletions
173
src/Service.Tests/UnitTests/DwSqlQueryBuilderUpsertTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
|
Comment on lines
+4
to
+8
|
||
| 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 | ||
| { | ||
| /// <summary> | ||
| /// 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). | ||
| /// </summary> | ||
| [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); | ||
|
|
||
| /// <summary> | ||
| /// Maps exposed field names to backing column names (identity mapping for the test entity). | ||
| /// </summary> | ||
| private static readonly Dictionary<string, string> _columnMapping = new() | ||
| { | ||
| { "id", "id" }, | ||
| { "title", "title" }, | ||
| { "publisher_id", "publisher_id" } | ||
| }; | ||
|
|
||
| /// <summary> | ||
| /// 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. | ||
| /// </summary> | ||
| [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}"); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Builds a minimal <see cref="SqlUpsertQueryStructure"/> for the test entity using a mocked | ||
| /// metadata provider so no live database is required. | ||
| /// </summary> | ||
| 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<ISqlMetadataProvider> metadataProvider = new(); | ||
| metadataProvider.Setup(x => x.EntityToDatabaseObject) | ||
| .Returns(new Dictionary<string, DatabaseObject> { { 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<string>(), It.IsAny<string>(), 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)); | ||
|
Comment on lines
+130
to
+134
|
||
|
|
||
| string outExposed; | ||
| metadataProvider.Setup(x => x.TryGetExposedColumnName(It.IsAny<string>(), It.IsAny<string>(), 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)); | ||
|
Comment on lines
+136
to
+140
|
||
|
|
||
| // The update policy is injected directly onto the structure, so the resolver only needs | ||
| // to return an empty policy (no throw) during construction. | ||
| Mock<IAuthorizationResolver> authorizationResolver = new(); | ||
| authorizationResolver | ||
| .Setup(x => x.ProcessDBPolicy(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<EntityActionOperation>(), It.IsAny<HttpContext>())) | ||
| .Returns(string.Empty); | ||
|
|
||
| RuntimeConfigProvider runtimeConfigProvider = TestHelper.GetRuntimeConfigProvider(TestHelper.GetRuntimeConfigLoader()); | ||
| Mock<IMetadataProviderFactory> metadataProviderFactory = new(); | ||
| GQLFilterParser gQLFilterParser = new(runtimeConfigProvider, metadataProviderFactory.Object); | ||
|
|
||
| DefaultHttpContext httpContext = new(); | ||
| httpContext.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER] = "authenticated"; | ||
|
|
||
| Dictionary<string, object?> 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); | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there are no pkPredicates at all, will this default WHERE to 1=1?
Or is pkPredicates always going to be non-empty?