Skip to content

Fix MySQL PUT/PATCH regarding row-level update database policy - #3734

Open
aaronburtle wants to merge 4 commits into
mainfrom
dev/aaronburtle/PUT-fix-mysql
Open

Fix MySQL PUT/PATCH regarding row-level update database policy#3734
aaronburtle wants to merge 4 commits into
mainfrom
dev/aaronburtle/PUT-fix-mysql

Conversation

@aaronburtle

@aaronburtle aaronburtle commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Why make this change?

On MySQL / Azure Database for MySQL, the REST PUT (upsert) and PATCH (incremental upsert) paths did not enforce the row-level update database policy configured for an entity. We now respect that row level database policy.

What is this change?

MySqlQueryBuilder.Build(SqlUpsertQueryStructure) previously did not retrieve or apply GetDbPolicyForOperation(EntityActionOperation.Update) on the upsert path.

The MySQL upsert now:

  • Uses an atomic INSERT ... ON DUPLICATE KEY UPDATE with a no-op duplicate branch, preventing concurrent same-key upserts from racing or deadlocking.
  • Uses @existed, set inside the duplicate branch, to distinguish inserts from updates without relying on ROW_COUNT() semantics.
  • Applies UPDATE ... WHERE <pk> AND <update-policy> only when the row already existed.
  • Uses a separate matched-row indicator so authorized idempotent updates return 200 even when UseAffectedRows=true.
  • Returns 200 for updates, 201 for inserts, 403 DatabasePolicyFailure when the update policy blocks an existing row, and 404 when an update-only record is not found.

MySQL integration coverage now includes:

  • Authorized and unauthorized PUT/PATCH updates.
  • Verification that policy-denied writes leave the row unchanged.
  • Idempotent PUT/PATCH with UseAffectedRows=true.
  • Repeated concurrent PUT requests targeting the same initially-missing primary key.
  • The database_policy_tester role and update policy in the generated MySQL test configuration.

Create-action database policies remain unsupported on MySQL, so create-policy-dependent tests remain skipped.

How was this tested?

  • Integration Tests
  • Unit Tests

Validated against MySQL 8.0.46 using .NET 10 (SDK 10.0.301).

  • Full MySQL PUT/PATCH/upsert regression: 73 passed, 0 failed, 12 skipped.
  • Idempotent PUT and PATCH with UseAffectedRows=true return 200.
  • Unauthorized PUT and PATCH return 403 DatabasePolicyFailure and leave the row unchanged.
  • Concurrent same-key PUT coverage repeats the scenario across 15 keys and verifies exactly one 201, one 200, one persisted row, and no 5xx response for each iteration.

Sample Request(s)

Entity commodities (table stocks) with update policy @item.pieceid ne 1 for role database_policy_tester.

Blocked update targeting a row that violates the policy:

PATCH /api/commodities/categoryid/0/pieceid/1
Authorization: Bearer <jwt>
X-MS-API-ROLE: database_policy_tester
Content-Type: application/json

{
  "categoryName": "SciFi",
  "piecesRequired": 5,
  "piecesAvailable": 2
}

Before the fix: 200 OK; the row was overwritten.

After the fix: 403 Forbidden (DatabasePolicyFailure); the row remains unchanged.

Allowed update targeting a row that satisfies the policy:

PUT /api/commodities/categoryid/100/pieceid/99
Authorization: Bearer <jwt>
X-MS-API-ROLE: database_policy_tester
Content-Type: application/json

{
  "categoryName": "SciFi",
  "piecesAvailable": 4,
  "piecesRequired": 5
}

Result: 200 OK; the row is updated.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the MySQL REST upsert implementation (PUT/PATCH) to enforce the entity’s row-level update database policy, aligning MySQL behavior with other engines by restructuring the upsert into multiple statements/result sets and enabling previously skipped MySQL policy integration tests.

Changes:

  • Restructured MySQL upsert SQL generation to: pre-count existing PK rows, attempt an UPDATE ... WHERE <pk> AND <update-policy>, then conditionally INSERT only when the PK doesn’t exist.
  • Added MySQL executor logic to interpret multiple result sets and return the correct HTTP outcome (update vs insert vs policy failure vs not found).
  • Enabled MySQL integration tests for update-policy scenarios and updated MySQL config generation + snapshot to include the new policy-testing role.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/Core/Resolvers/MySqlQueryBuilder.cs Reworked MySQL upsert SQL to enforce update policy and avoid ON DUPLICATE KEY UPDATE limitations.
src/Core/Resolvers/MySqlQueryExecutor.cs Added multi-result-set interpretation for MySQL upsert outcomes and policy failures.
src/Service.Tests/SqlTests/RestApiTests/Put/MySqlPutApiTests.cs Enabled PUT update-policy test and added MySQL validation query for it.
src/Service.Tests/SqlTests/RestApiTests/Patch/MySqlPatchApiTests.cs Enabled PATCH update-policy tests and added MySQL validation query for it.
src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt Updated snapshot to include database_policy_tester role with update DB policy.
config-generators/mysql-commands.txt Added commands to grant database_policy_tester permissions and update DB policy for Stock.

Comment thread src/Core/Resolvers/MySqlQueryBuilder.cs Outdated
Comment thread src/Core/Resolvers/MySqlQueryBuilder.cs Outdated
aaronburtle and others added 3 commits July 23, 2026 17:03
…EY UPDATE

Replace the FOR UPDATE gap-lock reads (which could deadlock, error 1213, when two concurrent RepeatableRead transactions both gap-lock the same missing key) with an atomic INSERT ... ON DUPLICATE KEY UPDATE no-op that serializes concurrent same-key inserts on the unique index. Detect insert-vs-existing via a session variable set inside the ON DUPLICATE branch (@Existed) rather than ROW_COUNT(), which is unreliable under UseAffectedRows=false. Also reuse the inherited _Composite_NonAutoGenPK_EntityPath constant to satisfy the IDE1006 const naming rule.
@aaronburtle aaronburtle added bug Something isn't working assign-for-review labels Jul 28, 2026
@aaronburtle aaronburtle added this to the July 2026 milestone Jul 28, 2026
@aaronburtle aaronburtle moved this from Todo to Review In Progress in Data API builder Jul 28, 2026
@aaronburtle aaronburtle self-assigned this Jul 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

Status: Review In Progress

Development

Successfully merging this pull request may close these issues.

4 participants