Can I simplify this MERGE statement w.r.t. WHEN MATCHED conditions?

I’m using SQL Server (SQL Server 2016 and Azure SQL) and I have this MERGE statement which uses a rather gnarly WHEN MATCHED condition to only update rows where values are actually different.

This is done for two reasons:

  1. The table has a rowversion column that will change when an UPDATE operation is performed, even if all of the values are the same. The rowversion values are useful for reducing client-activity (the application uses rowversion for optimistic concurrency).
  2. The table is also a Temporal Table, and SQL Server’s implementation of Temporal Tables will add copies of live data to the History table whenever an UPDATE is performed, even if no values are actually changed.
CREATE PROCEDURE UpsertItems
    @groupId int,
    @items   dbo.ItemsList READONLY -- This is a table-valued parameter. The UDT Table-Type has the same design as the `dbo.Items` table.

WITH existing AS    -- Using a CTE as the MERGE target to allow *safe* use of `WHEN NOT MATCHED BY SOURCE THEN DELETE` and apparently it's good for performance.
(
    SELECT
        groupId,
        itemId,
        a,
        b,
        c,
        d,
        e,
        f,
        -- etc
    FROM
        dbo.Items
    WHERE
        groupId = @groupId
)
MERGE INTO existing WITH (HOLDLOCK) AS tgt
USING
    @items AS src ON tgt.itemId = src.itemId
WHEN MATCHED AND
(
    -- This part is painful, but unfortunately these are all NULLable columns so they need the full `x IS DISTINCT FROM y`-equivalent comparison:

    ( ( tgt.a <> src.a OR tgt.a IS NULL OR src.a IS NULL ) AND NOT ( tgt.a IS NULL AND src.a IS NULL ) )
    OR
    ( ( tgt.b <> src.b OR tgt.b IS NULL OR src.b IS NULL ) AND NOT ( tgt.b IS NULL AND src.b IS NULL ) )
    OR
    ( ( tgt.c <> src.c OR tgt.c IS NULL OR src.c IS NULL ) AND NOT ( tgt.c IS NULL AND src.c IS NULL ) )
    OR
    ( ( tgt.d <> src.d OR tgt.d IS NULL OR src.d IS NULL ) AND NOT ( tgt.d IS NULL AND src.d IS NULL ) )
    OR
    ( ( tgt.e <> src.e OR tgt.e IS NULL OR src.e IS NULL ) AND NOT ( tgt.e IS NULL AND src.e IS NULL ) )
    OR
    ( ( tgt.f <> src.f OR tgt.f IS NULL OR src.f IS NULL ) AND NOT ( tgt.f IS NULL AND src.f IS NULL ) )
    -- etc
)
THEN UPDATE SET
    tgt.a = src.a,
    tgt.b = src.b,
    tgt.c = src.c,
    tgt.d = src.d,
    tgt.e = src.e,
    tgt.f = src.f,
    -- etc
WHEN NOT MATCHED BY TARGET THEN INSERT (
    groupId,
    itemId,
    a,
    b,
    c,
    d,
    e,
    f,
    -- etc
)
VALUES (
    src.groupId,
    src.itemId,
    src.a,
    src.b,
    src.c,
    src.d,
    src.e,
    src.f,
    -- etc
)
WHEN NOT MATCHED BY SOURCE THEN DELETE

OUTPUT
    $action AS [Action],

    inserted.groupId AS Ins_groupId,
    deleted .groupId AS Del_groupId,
    inserted.itemId  AS Ins_itemId,
    deleted .itemId  AS Del_itemId,
    inserted.a       AS Ins_a,
    deleted .a       AS Del_a,
    inserted.b       AS Ins_b,
    deleted .b       AS Del_b,
    inserted.c       AS Ins_c,
    deleted .c       AS Del_c,
    inserted.d       AS Ins_d,
    deleted .d       AS Del_d,
    inserted.e       AS Ins_e,
    deleted .e       AS Del_e,
    inserted.f       AS Ins_f,
    deleted .f       AS Del_f,
    -- etc
;

As you can see, this is quit the pain to maintain!

I already use tools like T4 to automate generating the repetitive parts of this query, but the sheer… scale and pain of this MERGE statement makes me feel like I’m doing something very wrong (because software is meant to light-the-way via the Pit of Success, so if one is encountering difficulties trying to do the right thing you’re probably doing it wrong), but I can’t think or see a better way of accomplishing this (BULK INSERT notwithstanding, but for the purposes of this question that’s not a possibility).

I know this statement can be simplified in other RDBMS which support x IS DISTINCT FROM y (which replaces the horrible-but-necessary NULL-safe checks in the WHEN MATCHED AND, but SQL Server still doesn’t support it.

Another pain is the lack of DRY in SQL in general – and the difficulty of implementing a DRY database in SQL Server (e.g. there’s no support for Deferred Constraints or Table Inheritance so you can’t implement the Subclass Table Pattern, which means needless repetition of data-design in multiple tables and weaker constraints) – but that’s another topic. I’m just frustrated with how backward SQL programming seems today compared to the many time-saving and keypress-saving features in modern languages like Kotlin and TypeScript.

Fantasy time:

I’d love to be able to do something like this, and not have to do with any gotchas (like how MERGE is unsafe by default without an explicit HOLDLOCK – that’s crazy!):

MERGE INTO
    dbo.Items AS tgt
WHERE
    tgt.groupId = @groupId
FROM
    @items AS src
ON
    tgt.itemId = src.itemId
WHEN MATCHED AND DIFFERENT THEN UPDATE ( automap )
WHEN NOT MATCHED BY TARGET THEN INSERT ( automap )
WHEN NOT MATCHED BY SOURCE THEN DELETE
OUTPUT ALL;

(Where automap would be an amazing feature in SQL Server that automatically maps source and destination columns by name and throws a compile-time error if it cannot automatically map columns to each other), and OUTPUT ALL would output all $action, inserted, and deleted values with different column names – using the same column names but with inserted and deleted values in adjacent rows).

Go to Source
Author: Dai

Why is Git Windows committing on merge, even with merge.commit=no?

I am using Git Bash in Windows 10, version: git version 2.25.1.windows.1. Let me know if I need to be more specific. I am also using GitExtensions but my question is around merging from Git Bash.

When I merge from there, i.e.:

git merge feature-branch-name

it commits even though, as far as I can tell, all three of my Git config files are set otherwise. I know I can specify --no-commit in the command but I would like not to have to do that.

From the source code directory, git config --list produces the output below, where it shows three times that merge.commit=no.

diff.astextplain.textconv=astextplain
filter.lfs.clean=git-lfs clean -- %f
filter.lfs.smudge=git-lfs smudge -- %f
filter.lfs.process=git-lfs filter-process
filter.lfs.required=true
http.sslbackend=openssl
http.sslcainfo=C:/Program Files/Git/mingw64/ssl/certs/ca-bundle.crt
core.autocrlf=false
core.fscache=true
core.symlinks=false
core.editor="C:\Program Files\Notepad++\notepad++.exe" -multiInst -notabbar -nosession -noPlugin
credential.helper=manager
merge.ff=no
merge.commit=no
core.editor="C:/Program Files (x86)/GitExtensions/GitExtensions.exe" fileeditor
user.email=craig@wereallconnected.ca
user.name=Craig Silver
merge.tool=winmerge
merge.ff=no
merge.commit=no
mergetool.winmerge.path=C:/Program Files (x86)/WinMerge/winmergeu.exe
mergetool.winmerge.cmd="C:/Program Files (x86)/WinMerge/winmergeu.exe" -e -u  -wl -wr -fm -dl "Mine: $LOCAL" -dm "Merged: $BASE" -dr "Theirs: $REMOTE" "$LOCAL" "$BASE" "$REMOTE" -o "$MERGED"
pull.rebase=false
fetch.prune=false
rebase.autostash=false
diff.guitool=winmerge
difftool.winmerge.path=C:/Program Files (x86)/WinMerge/winmergeu.exe
difftool.winmerge.cmd="C:/Program Files (x86)/WinMerge/winmergeu.exe" -e -u "$LOCAL" "$REMOTE"
core.repositoryformatversion=0
core.filemode=false
core.bare=false
core.logallrefupdates=true
core.ignorecase=true
core.sshcommand=ssh
merge.ff=no
merge.commit=no
submodule.active=.
remote.origin.url=REMOVED
remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
remote.origin.puttykeyfile=REMOVED
branch.master.remote=origin
branch.master.merge=refs/heads/master
branch.FMS-1203_data-structures-algorithms-string-matching.remote=origin
branch.FMS-1203_data-structures-algorithms-string-matching.merge=refs/heads/FMS-1203_data-structures-algorithms-string-matching
branch.FMS-1205_recency-trumps-frequency-for-small-fr-diff.remote=origin
branch.FMS-1205_recency-trumps-frequency-for-small-fr-diff.merge=refs/heads/FMS-1205_recency-trumps-frequency-for-small-fr-diff
branch.FMS-1204_debug-window.remote=origin
branch.FMS-1204_debug-window.merge=refs/heads/FMS-1204_debug-window

Also, git config --get merge.commit outputs no.

FYI, GitExtensions behaves correctly: merging there does not commit.

What am I missing?

Go to Source
Author: Craig Silver