@@ -149,6 +149,67 @@ public virtual string BuildInsert(InsertStatement insertStatement)
149149 return StringBuilderCache . ToString ( insertBuilder ) ;
150150 }
151151
152+ /// <summary>
153+ /// Builds a SQL UPSERT statement for SQL Server using MERGE syntax, including support for OUTPUT and comments.
154+ /// </summary>
155+ /// <param name="upsertStatement">The <see cref="UpsertStatement"/> containing the UPSERT statement configuration.</param>
156+ /// <returns>A SQL UPSERT statement string for SQL Server.</returns>
157+ /// <exception cref="ArgumentNullException">Thrown if <paramref name="upsertStatement"/> is <c>null</c>.</exception>
158+ /// <exception cref="ArgumentException">Thrown if the table, values, keys, or update values are not specified.</exception>
159+ public virtual string BuildUpsert ( UpsertStatement upsertStatement )
160+ {
161+ ValidateUpsert ( upsertStatement ) ;
162+
163+ var upsertBuilder = StringBuilderCache . Acquire ( ) ;
164+
165+ if ( upsertStatement . CommentExpressions ? . Count > 0 )
166+ {
167+ upsertBuilder
168+ . AppendJoin ( Environment . NewLine , upsertStatement . CommentExpressions )
169+ . AppendLine ( ) ;
170+ }
171+
172+ var table = TableExpression ( upsertStatement . TableExpression ) ;
173+ var sourceColumns = upsertStatement . ColumnExpressions . Select ( c => ColumnExpression ( c ) ) . ToArray ( ) ;
174+
175+ upsertBuilder
176+ . Append ( "MERGE INTO " )
177+ . Append ( table )
178+ . AppendLine ( " AS TARGET" )
179+ . Append ( "USING (VALUES (" )
180+ . AppendJoin ( ", " , upsertStatement . ValueExpressions )
181+ . AppendLine ( ")) AS SOURCE" )
182+ . Append ( "(" )
183+ . AppendJoin ( ", " , sourceColumns )
184+ . AppendLine ( ")" )
185+ . Append ( "ON " )
186+ . AppendJoin ( " AND " , upsertStatement . KeyExpressions . Select ( k => $ "TARGET.{ ColumnExpression ( k ) } = SOURCE.{ ColumnExpression ( k ) } ") )
187+ . AppendLine ( )
188+ . AppendLine ( "WHEN MATCHED THEN" )
189+ . Append ( "UPDATE SET " )
190+ . AppendJoin ( ", " , upsertStatement . UpdateExpressions . Select ( u => $ "TARGET.{ ColumnExpression ( u ) } = SOURCE.{ ColumnExpression ( u ) } ") )
191+ . AppendLine ( )
192+ . AppendLine ( "WHEN NOT MATCHED THEN" )
193+ . Append ( "INSERT (" )
194+ . AppendJoin ( ", " , sourceColumns )
195+ . AppendLine ( ")" )
196+ . Append ( "VALUES (" )
197+ . AppendJoin ( ", " , sourceColumns . Select ( c => $ "SOURCE.{ c } ") )
198+ . Append ( ')' ) ;
199+
200+ if ( upsertStatement . OutputExpressions ? . Count > 0 )
201+ {
202+ upsertBuilder
203+ . AppendLine ( )
204+ . Append ( "OUTPUT " )
205+ . AppendJoin ( ", " , upsertStatement . OutputExpressions . Select ( c => ColumnExpression ( c , "INSERTED" ) ) ) ;
206+ }
207+
208+ upsertBuilder . AppendLine ( ";" ) ;
209+
210+ return StringBuilderCache . ToString ( upsertBuilder ) ;
211+ }
212+
152213 /// <summary>
153214 /// Builds a SQL UPDATE statement for SQL Server, including support for OUTPUT, FROM, JOIN, WHERE, and comments.
154215 /// </summary>
@@ -584,6 +645,33 @@ public virtual string UpdateExpression(UpdateExpression updateExpression)
584645 return $ "{ quotedName } = { updateExpression . ParameterName } ";
585646 }
586647
648+ /// <summary>
649+ /// Validates that an UPSERT statement has the required table, values, keys, and update expressions.
650+ /// </summary>
651+ /// <param name="upsertStatement">The <see cref="UpsertStatement"/> to validate.</param>
652+ /// <exception cref="ArgumentNullException">Thrown if <paramref name="upsertStatement"/> is <c>null</c>.</exception>
653+ /// <exception cref="ArgumentException">Thrown if required upsert configuration is missing.</exception>
654+ protected static void ValidateUpsert ( UpsertStatement upsertStatement )
655+ {
656+ if ( upsertStatement is null )
657+ throw new ArgumentNullException ( nameof ( upsertStatement ) ) ;
658+
659+ if ( upsertStatement . TableExpression == null )
660+ throw new ArgumentException ( "No table specified to upsert into" , nameof ( upsertStatement ) ) ;
661+
662+ if ( upsertStatement . ColumnExpressions == null || upsertStatement . ColumnExpressions . Count == 0 )
663+ throw new ArgumentException ( "No columns specified for upsert" , nameof ( upsertStatement ) ) ;
664+
665+ if ( upsertStatement . ValueExpressions == null || upsertStatement . ValueExpressions . Count == 0 )
666+ throw new ArgumentException ( "No values specified for upsert" , nameof ( upsertStatement ) ) ;
667+
668+ if ( upsertStatement . KeyExpressions == null || upsertStatement . KeyExpressions . Count == 0 )
669+ throw new ArgumentException ( "No keys specified for upsert" , nameof ( upsertStatement ) ) ;
670+
671+ if ( upsertStatement . UpdateExpressions == null || upsertStatement . UpdateExpressions . Count == 0 )
672+ throw new ArgumentException ( "No update values specified for upsert" , nameof ( upsertStatement ) ) ;
673+ }
674+
587675 /// <summary>
588676 /// Builds a SQL JOIN expression from the specified <see cref="JoinExpression"/>.
589677 /// </summary>
0 commit comments