• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

loresoft / FluentCommand / 13359579719

16 Feb 2025 10:54PM UTC coverage: 54.174%. Remained the same
13359579719

push

github

pwelter34
Merge branch 'master' of https://github.com/loresoft/FluentCommand

1671 of 3562 branches covered (46.91%)

Branch coverage included in aggregate %.

4221 of 7314 relevant lines covered (57.71%)

227.44 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.99
/src/FluentCommand.SqlServer/Merge/DataMergeGenerator.cs
1
using System.Data;
2
using System.Text;
3

4
using FluentCommand.Extensions;
5
using FluentCommand.Internal;
6

7
namespace FluentCommand.Merge;
8

9
/// <summary>
10
/// Class used to build data merge SQL statements
11
/// </summary>
12
public static class DataMergeGenerator
13
{
14
    public const string OriginalPrefix = "Original";
15
    public const string CurrentPrefix = "Current";
16
    public const int TabSize = 4;
17

18
    /// <summary>
19
    /// Tables the identifier.
20
    /// </summary>
21
    /// <param name="name">The name.</param>
22
    /// <returns></returns>
23
    public static string TableIdentifier(string name)
24
    {
25
        var parts = name.Split('.');
22✔
26
        for (int i = 0; i < parts.Length; i++)
114✔
27
            parts[i] = QuoteIdentifier(parts[i]);
35✔
28

29
        return string.Join(".", parts);
22✔
30
    }
31

32
    public static string QuoteIdentifier(string name)
33
    {
34
        if (name.StartsWith("[") && name.EndsWith("]"))
683✔
35
            return name;
1✔
36

37
        return "[" + name.Replace("]", "]]") + "]";
682✔
38
    }
39

40
    public static string ParseIdentifier(string name)
41
    {
42
        if (name.StartsWith("[") && name.EndsWith("]"))
42✔
43
            return name.Substring(1, name.Length - 2);
3✔
44

45
        return name;
39✔
46
    }
47

48

49
    /// <summary>
50
    /// Builds the SQL for the temporary table used in the merge operation.
51
    /// </summary>
52
    /// <param name="mergeDefinition">The merge definition.</param>
53
    /// <returns></returns>
54
    public static string BuildTable(DataMergeDefinition mergeDefinition)
55
    {
56
        var builder = StringBuilderCache.Acquire();
4✔
57
        builder
4✔
58
            .Append("CREATE TABLE ")
4✔
59
            .Append(TableIdentifier(mergeDefinition.TemporaryTable))
4✔
60
            .AppendLine()
4✔
61
            .Append("(")
4✔
62
            .AppendLine();
4✔
63

64
        bool hasColumn = false;
4✔
65
        foreach (var mergeColumn in mergeDefinition.Columns.Where(c => !c.IsIgnored))
128✔
66
        {
67
            bool writeComma = hasColumn;
40✔
68

69
            builder
40✔
70
                .AppendLineIf(",", v => writeComma)
40✔
71
                .Append(' ', TabSize)
40✔
72
                .Append(QuoteIdentifier(mergeColumn.SourceColumn))
40✔
73
                .Append(" ")
40✔
74
                .Append(mergeColumn.NativeType)
40✔
75
                .Append(" NULL");
40✔
76

77
            hasColumn = true;
40✔
78
        }
79

80
        builder
4✔
81
            .AppendLine()
4✔
82
            .Append(")")
4✔
83
            .AppendLine();
4✔
84

85
        return StringBuilderCache.ToString(builder);
4✔
86
    }
87

88
    /// <summary>
89
    /// Builds the SQL merge statement for the merge operation.
90
    /// </summary>
91
    /// <param name="mergeDefinition">The merge definition.</param>
92
    /// <returns>The merge sql statement</returns>
93
    public static string BuildMerge(DataMergeDefinition mergeDefinition)
94
    {
95
        return BuildMerge(mergeDefinition, null);
5✔
96
    }
97

98
    /// <summary>
99
    /// Builds the SQL merge statement for the merge operation.
100
    /// </summary>
101
    /// <param name="mergeDefinition">The merge definition.</param>
102
    /// <param name="reader">The data reader to generate merge statement with.</param>
103
    /// <returns>The merge sql statement</returns>
104
    public static string BuildMerge(DataMergeDefinition mergeDefinition, IDataReader reader)
105
    {
106
        var mergeColumns = mergeDefinition.Columns
13✔
107
            .Where(c => !c.IsIgnored && (c.IsKey || c.CanInsert || c.CanUpdate))
113!
108
            .ToList();
13✔
109

110
        var builder = StringBuilderCache.Acquire();
13✔
111

112
        if (mergeDefinition.IdentityInsert && mergeDefinition.IncludeInsert)
13!
113
        {
114
            builder
×
115
                .Append("SET IDENTITY_INSERT ")
×
116
                .Append(TableIdentifier(mergeDefinition.TargetTable))
×
117
                .AppendLine(" ON;")
×
118
                .AppendLine();
×
119
        }
120

121

122
        builder
13✔
123
            .Append("MERGE INTO ")
13✔
124
            .Append(TableIdentifier(mergeDefinition.TargetTable))
13✔
125
            .Append(" AS t")
13✔
126
            .AppendLine();
13✔
127

128
        if (reader == null)
13✔
129
            AppendUsingSelect(mergeDefinition, mergeColumns, builder);
5✔
130
        else
131
            AppendUsingData(mergeDefinition, mergeColumns, reader, builder);
8✔
132

133
        AppendJoin(mergeColumns, builder);
13✔
134

135
        // Insert
136
        AppendInsert(mergeDefinition, builder);
13✔
137

138
        // Update
139
        AppendUpdate(mergeDefinition, builder);
13✔
140

141
        // Delete
142
        AppendDelete(mergeDefinition, builder);
13✔
143

144
        // Output
145
        AppendOutput(mergeDefinition, builder);
13✔
146

147
        // merge must end with ;
148
        builder.Append(";");
13✔
149

150
        if (mergeDefinition.IdentityInsert && mergeDefinition.IncludeInsert)
13!
151
        {
152
            builder
×
153
                .Append("SET IDENTITY_INSERT ")
×
154
                .Append(TableIdentifier(mergeDefinition.TargetTable))
×
155
                .AppendLine(" OFF;")
×
156
                .AppendLine();
×
157
        }
158

159
        return StringBuilderCache.ToString(builder);
13✔
160
    }
161

162

163
    private static void AppendUsingData(DataMergeDefinition mergeDefinition, List<DataMergeColumn> mergeColumns, IDataReader reader, StringBuilder builder)
164
    {
165
        builder
8✔
166
            .AppendLine("USING")
8✔
167
            .AppendLine("(")
8✔
168
            .Append(' ', TabSize)
8✔
169
            .AppendLine("VALUES");
8✔
170

171
        var fields = new HashSet<string>();
8✔
172

173
        bool wroteRow = false;
8✔
174
        while (reader.Read())
418✔
175
        {
176
            bool wrote = false;
410✔
177

178
            builder
410✔
179
                .AppendLineIf(", ", s => wroteRow)
410✔
180
                .Append(' ', TabSize)
410✔
181
                .Append("(");
410✔
182

183
            for (int i = 0; i < reader.FieldCount; i++)
5,814✔
184
            {
185
                var fieldName = reader.GetName(i);
2,497✔
186

187
                var isFound = mergeColumns.Any(c => c.SourceColumn == fieldName);
11,749✔
188
                if (!isFound)
2,497✔
189
                    continue;
190

191
                fields.Add(fieldName);
2,497✔
192

193
                builder.AppendIf(", ", v => wrote);
4,994✔
194

195
                var value = reader.GetValue(i);
2,497✔
196
                var stringValue = GetValue(value);
2,497✔
197
                var fieldType = reader.GetFieldType(i);
2,497✔
198

199
                // ReSharper disable once ConditionIsAlwaysTrueOrFalse
200
                if (value != null && value != DBNull.Value && NeedQuote(fieldType))
2,497✔
201
                    builder.AppendFormat("'{0}'", stringValue.Replace("'", "''"));
1,655✔
202
                else
203
                    builder.Append(stringValue);
842✔
204

205

206
                wrote = true;
2,497✔
207
            }
208
            builder.Append(")");
410✔
209

210
            wroteRow = true;
410✔
211
        }
212

213
        builder
8✔
214
            .AppendLine()
8✔
215
            .AppendLine(")")
8✔
216
            .AppendLine("AS s")
8✔
217
            .AppendLine("(")
8✔
218
            .Append(' ', TabSize);
8✔
219

220
        bool wroteColumn = false;
8✔
221
        foreach (var field in fields)
150✔
222
        {
223
            if (wroteColumn)
67✔
224
                builder.Append(", ");
59✔
225

226
            builder.Append(QuoteIdentifier(field));
67✔
227
            wroteColumn = true;
67✔
228
        }
229

230
        builder
8✔
231
            .AppendLine()
8✔
232
            .AppendLine(")");
8✔
233
    }
8✔
234

235
    private static void AppendUsingSelect(DataMergeDefinition mergeDefinition, List<DataMergeColumn> mergeColumns, StringBuilder builder)
236
    {
237
        builder
5✔
238
            .AppendLine("USING")
5✔
239
            .AppendLine("(")
5✔
240
            .Append(' ', TabSize)
5✔
241
            .AppendLine("SELECT");
5✔
242

243
        bool hasColumn = false;
5✔
244
        foreach (var mergeColumn in mergeColumns)
102✔
245
        {
246
            bool writeComma = hasColumn;
46✔
247

248
            builder
46✔
249
                .AppendLineIf(",", v => writeComma)
46✔
250
                .Append(' ', TabSize * 2)
46✔
251
                .Append(QuoteIdentifier(mergeColumn.SourceColumn));
46✔
252

253
            hasColumn = true;
46✔
254
        }
255

256
        builder
5✔
257
            .AppendLine()
5✔
258
            .Append(' ', TabSize)
5✔
259
            .Append("FROM ")
5✔
260
            .Append(TableIdentifier(mergeDefinition.TemporaryTable))
5✔
261
            .AppendLine()
5✔
262
            .AppendLine(")")
5✔
263
            .AppendLine("AS s");
5✔
264
    }
5✔
265

266
    private static void AppendJoin(List<DataMergeColumn> mergeColumns, StringBuilder builder)
267
    {
268
        bool hasColumn;
269
        builder
13✔
270
            .AppendLine("ON")
13✔
271
            .AppendLine("(");
13✔
272

273
        hasColumn = false;
13✔
274
        foreach (var mergeColumn in mergeColumns.Where(c => c.IsKey))
165✔
275
        {
276
            bool writeComma = hasColumn;
13✔
277
            builder
13✔
278
                .AppendLineIf(" AND ", v => writeComma)
13✔
279
                .Append(' ', TabSize)
13✔
280
                .Append("t.")
13✔
281
                .Append(QuoteIdentifier(mergeColumn.TargetColumn))
13✔
282
                .Append(" = s.")
13✔
283
                .Append(QuoteIdentifier(mergeColumn.SourceColumn));
13✔
284

285
            hasColumn = true;
13✔
286
        }
287

288
        builder
13✔
289
            .AppendLine()
13✔
290
            .Append(")")
13✔
291
            .AppendLine();
13✔
292
    }
13✔
293

294
    private static void AppendOutput(DataMergeDefinition mergeDefinition, StringBuilder builder)
295
    {
296
        if (!mergeDefinition.IncludeOutput)
13✔
297
            return;
10✔
298

299
        var mergeColumns = mergeDefinition.Columns
3✔
300
            .Where(c => !c.IsIgnored)
18✔
301
            .ToList();
3✔
302

303
        builder
3✔
304
            .AppendLine("OUTPUT")
3✔
305
            .Append(' ', TabSize)
3✔
306
            .Append("$action as [Action]");
3✔
307

308
        foreach (var mergeColumn in mergeColumns)
42✔
309
        {
310
            builder
18✔
311
                .AppendLine(",")
18✔
312
                .Append(' ', TabSize)
18✔
313
                .Append("DELETED.")
18✔
314
                .Append(QuoteIdentifier(mergeColumn.SourceColumn))
18✔
315
                .Append(" as [")
18✔
316
                .Append(OriginalPrefix)
18✔
317
                .Append(ParseIdentifier(mergeColumn.SourceColumn))
18✔
318
                .Append("],")
18✔
319
                .AppendLine();
18✔
320

321
            builder
18✔
322
                .Append(' ', TabSize)
18✔
323
                .Append("INSERTED.")
18✔
324
                .Append(QuoteIdentifier(mergeColumn.SourceColumn))
18✔
325
                .Append(" as [")
18✔
326
                .Append(CurrentPrefix)
18✔
327
                .Append(ParseIdentifier(mergeColumn.SourceColumn))
18✔
328
                .Append("]");
18✔
329
        }
330

331
    }
3✔
332

333
    private static void AppendDelete(DataMergeDefinition mergeDefinition, StringBuilder builder)
334
    {
335
        if (!mergeDefinition.IncludeDelete)
13!
336
            return;
13✔
337

338
        builder
×
339
            .AppendLine("WHEN NOT MATCHED BY SOURCE THEN ")
×
340
            .Append(' ', TabSize)
×
341
            .AppendLine("DELETE");
×
342
    }
×
343

344
    private static void AppendUpdate(DataMergeDefinition mergeDefinition, StringBuilder builder)
345
    {
346
        if (!mergeDefinition.IncludeUpdate)
13!
347
            return;
×
348

349
        var mergeColumns = mergeDefinition.Columns
13✔
350
            .Where(c => !c.IsIgnored && c.CanUpdate)
113!
351
            .ToList();
13✔
352

353
        builder
13✔
354
            .AppendLine("WHEN MATCHED THEN ")
13✔
355
            .Append(' ', TabSize)
13✔
356
            .AppendLine("UPDATE SET");
13✔
357

358
        bool hasColumn = false;
13✔
359
        foreach (var mergeColumn in mergeColumns)
226✔
360
        {
361
            bool writeComma = hasColumn;
100✔
362
            builder
100✔
363
                .AppendLineIf(",", v => writeComma)
100✔
364
                .Append(' ', TabSize * 2)
100✔
365
                .Append("t.")
100✔
366
                .Append(QuoteIdentifier(mergeColumn.TargetColumn))
100✔
367
                .Append(" = s.")
100✔
368
                .Append(QuoteIdentifier(mergeColumn.SourceColumn));
100✔
369

370
            hasColumn = true;
100✔
371
        }
372
        builder.AppendLine();
13✔
373
    }
13✔
374

375
    private static void AppendInsert(DataMergeDefinition mergeDefinition, StringBuilder builder)
376
    {
377
        if (!mergeDefinition.IncludeInsert)
13!
378
            return;
×
379

380
        var mergeColumns = mergeDefinition.Columns
13✔
381
            .Where(c => !c.IsIgnored && c.CanInsert)
113!
382
            .ToList();
13✔
383

384
        builder
13✔
385
            .AppendLine("WHEN NOT MATCHED BY TARGET THEN ")
13✔
386
            .Append(' ', TabSize)
13✔
387
            .AppendLine("INSERT")
13✔
388
            .Append(' ', TabSize)
13✔
389
            .AppendLine("(");
13✔
390

391
        bool hasColumn = false;
13✔
392
        foreach (var mergeColumn in mergeColumns)
252✔
393
        {
394
            bool writeComma = hasColumn;
113✔
395
            builder
113✔
396
                .AppendLineIf(",", v => writeComma)
113✔
397
                .Append(' ', TabSize * 2)
113✔
398
                .Append(QuoteIdentifier(mergeColumn.TargetColumn));
113✔
399

400
            hasColumn = true;
113✔
401
        }
402
        builder.AppendLine();
13✔
403

404
        builder
13✔
405
            .Append(' ', TabSize)
13✔
406
            .AppendLine(")")
13✔
407
            .Append(' ', TabSize)
13✔
408
            .AppendLine("VALUES")
13✔
409
            .Append(' ', TabSize)
13✔
410
            .AppendLine("(");
13✔
411

412
        hasColumn = false;
13✔
413
        foreach (var mergeColumn in mergeColumns)
252✔
414
        {
415
            bool writeComma = hasColumn;
113✔
416
            builder
113✔
417
                .AppendLineIf(",", v => writeComma)
113✔
418
                .Append(' ', TabSize * 2)
113✔
419
                .Append("s.")
113✔
420
                .Append(QuoteIdentifier(mergeColumn.SourceColumn));
113✔
421

422
            hasColumn = true;
113✔
423
        }
424
        builder.AppendLine();
13✔
425

426
        builder
13✔
427
            .Append(' ', TabSize)
13✔
428
            .AppendLine(")");
13✔
429
    }
13✔
430

431

432
    private static bool NeedQuote(Type type)
433
    {
434
        var underType = type.GetUnderlyingType();
1,675✔
435

436
        if (underType == typeof(string))
1,675✔
437
            return true;
1,634✔
438
        if (underType == typeof(TimeSpan))
41✔
439
            return true;
3✔
440
        if (underType == typeof(DateTime))
38✔
441
            return true;
3✔
442
        if (underType == typeof(DateTimeOffset))
35✔
443
            return true;
3✔
444
        if (underType == typeof(Guid))
32✔
445
            return true;
6✔
446
#if NET6_0_OR_GREATER
447
        if (underType == typeof(DateOnly))
26✔
448
            return true;
3✔
449
        if (underType == typeof(TimeOnly))
23✔
450
            return true;
3✔
451
#endif
452

453
        return false;
20✔
454
    }
455

456
    private static string GetValue(object value)
457
    {
458
        if (value == null || value == DBNull.Value)
2,497✔
459
            return "NULL";
822✔
460

461
        return value switch
1,675!
462
        {
1,675✔
463
            string stringValue => stringValue,
1,634✔
464
            DateTime dateTimeValue => dateTimeValue.ToString("yyyy-MM-dd HH:mm:ss.fff"),
3✔
465
            DateTimeOffset dateTimeOffset => dateTimeOffset.ToString("yyyy-MM-dd HH:mm:ss.ffffffzzz"),
3✔
466
            byte[] byteArray => ToHex(byteArray),
×
467
            bool boolValue => boolValue ? "1" : "0",
3✔
468
#if NET6_0_OR_GREATER
1,675✔
469
            DateOnly dateValue => dateValue.ToString("yyyy-MM-dd"),
3✔
470
            TimeOnly timeValue => timeValue.ToString("hh:mm:ss.ffffff"),
3✔
471
#endif
1,675✔
472
            _ => Convert.ToString(value)
26✔
473
        };
1,675✔
474
    }
475

476
    private static string ToHex(byte[] bytes)
477
    {
478
#if NET5_0_OR_GREATER
479
        return Convert.ToHexString(bytes);
×
480
#else
481
        var s = StringBuilderCache.Acquire();
482
        s.Append("0x");
483

484
        foreach (var b in bytes)
485
            s.Append(b.ToString("x2").ToUpperInvariant());
486

487
        return StringBuilderCache.ToString(s);
488
#endif
489
    }
490
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc