Azure / Azure/data-api-builder

[Bug]: Concurrent same-key PUT/PATCH upserts can fail or create duplicate logical rows

Open
#3,743 0 comments 0 reactions 1 assignee Claimed by @aaronburtle View on GitHub
bug triage
Dominant language
C#
Stars
1.5k
Forks
370
Avg merge
3d 17h
Merged PRs (30d)
8

Description

## Title

Concurrent same-key PUT/PATCH upserts can fail or create duplicate logical rows on relational database providers

## Description

REST `PUT` and `PATCH` operations with a caller-supplied primary key perform an upsert. On SQL Server, PostgreSQL, and Data Warehouse SQL, the current implementation decides between `UPDATE` and `INSERT` using multiple database operations without first serializing requests for the same key.

At `READ COMMITTED`, two concurrent requests targeting the same missing primary key can both observe that the row does not exist and independently choose the insert path.

Depending on the provider:

- SQL Server can return a duplicate-key, conflict, or deadlock error for one request.
- PostgreSQL can return a unique-constraint or serialization-related failure for one request.
- Data Warehouse SQL may create duplicate logical rows because configured DAB key fields are not necessarily backed by an enforced unique constraint.

This is a time-of-check/time-of-use race. The operation is transactional, but the initial existence decision is not protected against another same-key upsert making the result stale before the insert occurs.

## Expected behavior

Concurrent upserts targeting the same entity and primary-key value should serialize before checking row existence:

- Exactly one request should insert the missing row and return `201 Created`.
- Subsequent concurrent requests should observe the inserted row, update it, and return `200 OK`.
- No request should fail with a duplicate-key, deadlock, conflict, or database-operation error.
- Exactly one logical row should remain for the key.
- Existing update/create database-policy behavior must remain unchanged.
- Upserts targeting different keys should remain concurrent where supported.

## Scope

Affected relational providers:

- SQL Server
- PostgreSQL
- Data Warehouse SQL / Azure Synapse

MySQL is excluded because its concurrent upsert path is addressed separately in PR #3734. Cosmos DB is also outside this issue's scope.

## Implementation considerations

Provider-specific serialization is required:

- SQL Server: acquire and hold an update/key-range lock for the PK existence decision.
- PostgreSQL: acquire a transaction-level advisory lock derived from the source and complete primary key before checking existence.
- Data Warehouse SQL: use a held exclusive source-table lock for insert-capable upserts because configured logical keys may not be physically enforced.

The lock must be held through the complete ambient upsert transaction and released automatically on commit, rollback, cancellation, or exception.

## Test coverage

Add concurrent same-key integration coverage for both `PUT` and `PATCH` where supported:

- Start several upserts against the same initially-missing key.
- Assert exactly one `201` and all remaining responses are `200`.
- Assert no `4xx`/`5xx` database-operation failures.
- Assert exactly one row remains for the logical key.

Add SQL-generation tests for provider-specific locking behavior, including composite primary keys and update-only fallback paths.

### Version

all

### What database are you using?

Azure SQL

### What hosting model are you using?

_No response_

### Which API approach are you accessing DAB through?

_No response_

### Relevant log output

```Text

```

### Code of Conduct

- [x] I agree to follow this project's Code of Conduct

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.