Skip to content

feat(mssql): support native upsert via MERGE WITH (HOLDLOCK)#5794

Open
MarkusLund wants to merge 1 commit into
prisma:mainfrom
MarkusLund:fix-mssql-native-upsert-holdlock
Open

feat(mssql): support native upsert via MERGE WITH (HOLDLOCK)#5794
MarkusLund wants to merge 1 commit into
prisma:mainfrom
MarkusLund:fix-mssql-native-upsert-holdlock

Conversation

@MarkusLund
Copy link
Copy Markdown

@MarkusLund MarkusLund commented Mar 10, 2026

Summary

Adds native upsert support for SQL Server by translating OnConflict::Update into MERGE ... WITH (HOLDLOCK), giving MSSQL the same atomic upsert semantics that PostgreSQL and SQLite already have via ON CONFLICT DO UPDATE.

Motivation

Currently, MSSQL upserts fall back to a transactional query graph: a SELECT to check existence, then either an INSERT or UPDATE depending on the result. While wrapped in a transaction, this multi-step approach can still surface unique constraint violations under concurrent load because the gap between the read and the write is not fully serializable.

MERGE WITH (HOLDLOCK) replaces this with a single atomic statement that holds a range lock during the match phase, eliminating the race condition entirely.

WITH (HOLDLOCK) is applied at the MSSQL MERGE visitor level, so it affects both the new OnConflict::Update path and the existing OnConflict::DoNothing path. This is intentional: both MERGE variants benefit from serializable-level locking on SQL Server.

Changes

  • Enable NativeUpsert capability for the MSSQL connector
  • Add Merge::from_insert_with_update() to convert INSERT ... ON CONFLICT UPDATE into MERGE ... WHEN MATCHED THEN UPDATE
  • Build MERGE ON predicates from explicit conflict columns (compound keys supported)
  • Preserve schema-qualified table references ([dbo].[table]) in generated ON clauses
  • Emit WITH (HOLDLOCK) on all MSSQL MERGE statements
  • Extract shared build_using_query() helper to avoid duplication between DoNothing and Update paths
  • Extend native_upsert.rs assertion helpers to detect MERGE INTO in addition to ON CONFLICT

Test plan

  • cargo test -p quaint test_native_upsert --lib — unit tests for rendered SQL (single unique, compound, schema-qualified, multi-row, conditions, returning, error cases)
  • cargo test -p quaint upsert -- --nocapture — SQLite integration passing locally
  • CI: MSSQL connector integration tests (query-engine-tests native upsert suite)
  • CI: PostgreSQL and SQLite native upsert tests unaffected
  • Manual verification against downstream SQL Server app with concurrent upsert race scenario

Summary by CodeRabbit

  • New Features

    • SQL Server native upsert support marked available; MERGE emits WITH (HOLDLOCK) and can include optional WHEN MATCHED updates.
  • Documentation

    • Upsert docs updated to show SQL Server MERGE alongside PostgreSQL/SQLite and include HOLDLOCK example.
  • Tests

    • Tests expanded to cover SQL Server native upsert; assertions accept MERGE or ON CONFLICT and new MSSQL cases added.
  • Refactor

    • Query compatibility hooks now surface errors during transformations; MERGE/upsert paths handle fallible conversions.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 10, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds MSSQL native upsert support: enables NativeUpsert capability, extends the Merge AST with WHEN MATCHED and builders to construct MERGE from INSERT ... ON CONFLICT UPDATE, updates the Visitor API and MSSQL visitor to emit MERGE INTO ... WITH (HOLDLOCK) and propagate errors, and adjusts tests/logging to recognize MERGE.

Changes

Cohort / File(s) Summary
Connector capability
psl/psl-core/src/builtin_connectors/mssql_datamodel_connector.rs
Enabled the NativeUpsert capability flag for the MsSql datamodel connector.
AST: Insert / Merge
quaint/src/ast/insert.rs, quaint/src/ast/merge.rs
Doc updates in insert.rs. Merge gains pub(crate) when_matched: Option<Update<'a>>, a when_matched setter, from_insert_with_update constructor, and helper functions build_using_query and build_on_conditions_from_constraints.
Visitor trait & MSSQL visitor
quaint/src/visitor.rs, quaint/src/visitor/mssql.rs
Visitor::compatibility_modifications now returns crate::Result<Query<'a>>. MSSQL visitor updated to produce fallible compatibility modifications, convert INSERT ... ON CONFLICT into Merge (including optional WHEN MATCHED conditions), and emit MERGE INTO ... WITH (HOLDLOCK) (with optional target alias).
Tests / native upsert detection
quaint/src/tests/upsert.rs, query-engine/connector-test-kit-rs/.../native_upsert.rs
Updated tests to include the mssql connector. Native upsert log/assert checks now accept either ON CONFLICT or MERGE INTO as indicators of native upsert.
Misc / Manifests
(various small edits)
Minor manifest and ancillary edits referenced by the changeset (non-API changes).
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly describes the main change: adding native upsert support for MSSQL via MERGE WITH (HOLDLOCK), which accurately reflects the primary objective of enabling atomic upsert semantics for SQL Server.
Docstring Coverage ✅ Passed Docstring coverage is 90.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
✨ Simplify code
  • Create PR with simplified code

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Mar 10, 2026

CLA assistant check
All committers have signed the CLA.

@MarkusLund MarkusLund force-pushed the fix-mssql-native-upsert-holdlock branch from c09666f to 4c0306b Compare March 12, 2026 14:55
@MarkusLund MarkusLund marked this pull request as ready for review March 12, 2026 15:33
Copilot AI review requested due to automatic review settings March 12, 2026 15:33
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 adds native upsert support for MSSQL by translating OnConflict::Update into MERGE ... WITH (HOLDLOCK) statements, providing atomic upsert semantics equivalent to PostgreSQL's and SQLite's ON CONFLICT DO UPDATE. This eliminates race conditions that previously existed with the transactional SELECT-then-INSERT/UPDATE fallback approach under concurrent load.

Changes:

  • Enables NativeUpsert capability for the MSSQL connector and adds Merge::from_insert_with_update() to convert INSERT ... ON CONFLICT UPDATE into MERGE ... WHEN MATCHED THEN UPDATE
  • Emits WITH (HOLDLOCK) on all MSSQL MERGE statements (both DoNothing and Update paths) and extracts a shared build_using_query() helper to eliminate duplication
  • Extends test assertion helpers and adds comprehensive unit tests for the new MERGE generation (single unique, compound, schema-qualified, multi-row, conditions, returning, error cases)

Reviewed changes

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

Show a summary per file
File Description
psl/psl-core/src/builtin_connectors/mssql_datamodel_connector.rs Adds NativeUpsert capability flag for MSSQL
quaint/src/ast/merge.rs Adds from_insert_with_update(), build_on_conditions_from_constraints(), and shared build_using_query() helper; adds when_matched field to Merge struct
quaint/src/ast/insert.rs Updates doc comment for OnConflict to reflect MSSQL support
quaint/src/visitor/mssql.rs Handles OnConflict::Update in compatibility_modifications, emits WITH (HOLDLOCK) and WHEN MATCHED THEN UPDATE SET in MERGE visitor; updates all existing test expectations
quaint/src/tests/upsert.rs Adds mssql tag to integration upsert tests
query-engine/connector-test-kit-rs/query-engine-tests/tests/new/native_upsert.rs Extends assertion helpers to detect MERGE INTO alongside ON CONFLICT

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c155a584-97dd-45cb-b89a-fda174b6e368

📥 Commits

Reviewing files that changed from the base of the PR and between 280c870 and 4c0306b.

📒 Files selected for processing (6)
  • psl/psl-core/src/builtin_connectors/mssql_datamodel_connector.rs
  • quaint/src/ast/insert.rs
  • quaint/src/ast/merge.rs
  • quaint/src/tests/upsert.rs
  • quaint/src/visitor/mssql.rs
  • query-engine/connector-test-kit-rs/query-engine-tests/tests/new/native_upsert.rs

Comment thread quaint/src/ast/merge.rs Outdated
Comment thread quaint/src/visitor/mssql.rs
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: fda4f8bd-2030-4a62-ad60-8847a1479dc5

📥 Commits

Reviewing files that changed from the base of the PR and between 4c0306b and 6cf63cd.

📒 Files selected for processing (3)
  • quaint/src/ast/merge.rs
  • quaint/src/visitor.rs
  • quaint/src/visitor/mssql.rs

Comment thread quaint/src/ast/merge.rs
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment thread quaint/src/visitor/mssql.rs Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 11f8f6d0-9b52-4fcc-ab71-c8b88e189052

📥 Commits

Reviewing files that changed from the base of the PR and between 4a76627 and c2ce504.

📒 Files selected for processing (1)
  • quaint/src/visitor/mssql.rs

Comment thread quaint/src/visitor/mssql.rs
@MarkusLund MarkusLund force-pushed the fix-mssql-native-upsert-holdlock branch from c2ce504 to 3416f7e Compare April 14, 2026 10:47
@MarkusLund
Copy link
Copy Markdown
Author

Changes since last review

Squashed all commits into a single clean commit addressing the review feedback:

  1. HOLDLOCK placement fixedWITH (HOLDLOCK) now correctly appears before the table alias per T-SQL syntax: MERGE INTO [table] WITH (HOLDLOCK) AS [alias] (was previously placed after the alias).

  2. ON clause alias handling — When the MERGE target has an alias, the ON conditions now reference the alias instead of the original table name. T-SQL requires this once an alias is declared. Added a regression test for aliased targets.

  3. Empty rows panic fixedrows.pop().unwrap() replaced with proper error propagation via ok_or_else.

  4. Row arity preserved — Switched from rows.pop() (which reverses order) to rows.into_iter().next() to maintain input row order.

  5. Error propagationVisitor::compatibility_modifications now returns Result so Merge construction errors propagate instead of panicking.

All existing MSSQL visitor tests (79) pass, plus 10 new native upsert tests.

Enable NativeUpsert capability for MSSQL by translating INSERT ... ON
CONFLICT UPDATE into MERGE ... WITH (HOLDLOCK) statements, providing
atomic upsert semantics equivalent to PostgreSQL's and SQLite's
ON CONFLICT DO UPDATE.

- Add Merge::from_insert_with_update() to convert INSERT with
  OnConflict::Update into MERGE with WHEN MATCHED THEN UPDATE
- Emit WITH (HOLDLOCK) on all MSSQL MERGE statements for
  serializable isolation under concurrent load
- Place WITH (HOLDLOCK) before table alias per T-SQL syntax rules
- Use alias in ON clause when merge target is aliased
- Extract shared build_using_query() helper to eliminate duplication
  between DoNothing and Update paths
- Make Visitor::compatibility_modifications return Result to propagate
  errors from Merge construction instead of panicking
@MarkusLund MarkusLund force-pushed the fix-mssql-native-upsert-holdlock branch from 3416f7e to a0f221c Compare May 6, 2026 06:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants