Skip to content

Commit 96088ce

Browse files
committed
Add soft-delete support to DataMerge
1 parent 570ff6f commit 96088ce

5 files changed

Lines changed: 123 additions & 0 deletions

File tree

‎src/FluentCommand.SqlServer/Merge/DataMerge.cs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,18 @@ public IDataMerge IncludeDelete(bool value = true)
5050
return this;
5151
}
5252

53+
/// <inheritdoc/>
54+
public IDataMerge SoftDelete(string column, object? value = null)
55+
{
56+
if (string.IsNullOrWhiteSpace(column))
57+
throw new ArgumentException("The soft delete column name is required.", nameof(column));
58+
59+
_mergeDefinition.SoftDeleteColumn = column;
60+
_mergeDefinition.SoftDeleteValue = value ?? true;
61+
_mergeDefinition.IncludeDelete = true;
62+
return this;
63+
}
64+
5365
/// <inheritdoc/>
5466
public IDataMerge IdentityInsert(bool value = true)
5567
{

‎src/FluentCommand.SqlServer/Merge/DataMergeDefinition.cs‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,27 @@ public DataMergeDefinition()
6565
/// </value>
6666
public bool IncludeDelete { get; set; }
6767

68+
/// <summary>
69+
/// Gets or sets the name of the column in the <see cref="TargetTable"/> used to flag rows as soft deleted.
70+
/// When <see cref="IncludeDelete"/> is <c>true</c> and this value is set, target rows not found in the source
71+
/// are updated with <see cref="SoftDeleteValue"/> instead of being deleted.
72+
/// </summary>
73+
/// <value>
74+
/// The soft delete column name, or <c>null</c> to use hard delete.
75+
/// </value>
76+
/// <remarks>
77+
/// Soft deleted rows are reported with an <c>UPDATE</c> action in merge output.
78+
/// </remarks>
79+
public string? SoftDeleteColumn { get; set; }
80+
81+
/// <summary>
82+
/// Gets or sets the value assigned to <see cref="SoftDeleteColumn"/> when a row is soft deleted.
83+
/// </summary>
84+
/// <value>
85+
/// The soft delete value.
86+
/// </value>
87+
public object? SoftDeleteValue { get; set; }
88+
6889
/// <summary>
6990
/// Gets or sets a value indicating whether to output the inserted, updated, or deleted values from the <see cref="TargetTable"/> after the merge operation.
7091
/// </summary>

‎src/FluentCommand.SqlServer/Merge/DataMergeGenerator.cs‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,32 @@ private static void AppendDelete(DataMergeDefinition mergeDefinition, StringBuil
379379
if (!mergeDefinition.IncludeDelete)
380380
return;
381381

382+
if (mergeDefinition.SoftDeleteColumn.HasValue())
383+
{
384+
var column = QuoteIdentifier(mergeDefinition.SoftDeleteColumn!);
385+
var value = mergeDefinition.SoftDeleteValue;
386+
var literal = GetValue(value);
387+
388+
if (value != null && value != DBNull.Value && NeedQuote(value.GetType()))
389+
literal = $"'{literal.Replace("'", "''")}'";
390+
391+
builder
392+
.Append("WHEN NOT MATCHED BY SOURCE AND (t.")
393+
.Append(column)
394+
.Append(" IS NULL OR t.")
395+
.Append(column)
396+
.Append(" <> ")
397+
.Append(literal)
398+
.AppendLine(") THEN ")
399+
.Append(' ', TabSize)
400+
.Append("UPDATE SET t.")
401+
.Append(column)
402+
.Append(" = ")
403+
.AppendLine(literal);
404+
405+
return;
406+
}
407+
382408
builder
383409
.AppendLine("WHEN NOT MATCHED BY SOURCE THEN ")
384410
.Append(' ', TabSize)

‎src/FluentCommand.SqlServer/Merge/IDataMerge.cs‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@ public interface IDataMerge
2828
/// <returns>The same <see cref="IDataMerge"/> instance for fluent chaining.</returns>
2929
IDataMerge IncludeDelete(bool value = true);
3030

31+
/// <summary>
32+
/// Specifies that target rows not found in the source data are soft deleted by setting <paramref name="column"/> to <paramref name="value"/>.
33+
/// This also enables <see cref="IncludeDelete(bool)"/>.
34+
/// </summary>
35+
/// <param name="column">The target column used to flag rows as deleted.</param>
36+
/// <param name="value">The value assigned to the column for soft deleted rows. Defaults to <c>true</c> when <c>null</c>.</param>
37+
/// <returns>The same <see cref="IDataMerge"/> instance for fluent chaining.</returns>
38+
/// <remarks>Soft deleted rows are reported with an <c>UPDATE</c> action in merge output.</remarks>
39+
IDataMerge SoftDelete(string column, object? value = null);
40+
3141
/// <summary>
3242
/// Specifies whether to allow identity insert on the target table during the merge operation.
3343
/// </summary>

‎test/FluentCommand.SqlServer.Tests/DataMergeGeneratorTests.cs‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,60 @@ public void QuoteIdentifier()
6868

6969
}
7070

71+
[Fact]
72+
public void BuildMergeSoftDeleteBoolean()
73+
{
74+
var definition = DataMergeDefinition.Create<UserImport>();
75+
definition.TargetTable = "dbo.User";
76+
definition.IncludeDelete = true;
77+
definition.SoftDeleteColumn = "IsDeleted";
78+
definition.SoftDeleteValue = true;
79+
80+
var sql = DataMergeGenerator.BuildMerge(definition);
81+
82+
sql.Should().Contain("WHEN NOT MATCHED BY SOURCE AND (t.[IsDeleted] IS NULL OR t.[IsDeleted] <> 1) THEN \r\n UPDATE SET t.[IsDeleted] = 1".ReplaceLineEndings());
83+
}
84+
85+
[Fact]
86+
public void BuildMergeSoftDeleteString()
87+
{
88+
var definition = DataMergeDefinition.Create<UserImport>();
89+
definition.TargetTable = "dbo.User";
90+
definition.IncludeDelete = true;
91+
definition.SoftDeleteColumn = "Status";
92+
definition.SoftDeleteValue = "O'Deleted";
93+
94+
var sql = DataMergeGenerator.BuildMerge(definition);
95+
96+
sql.Should().Contain("UPDATE SET t.[Status] = 'O''Deleted'");
97+
}
98+
99+
[Fact]
100+
public void BuildMergeHardDeleteWithoutSoftDeleteColumn()
101+
{
102+
var definition = DataMergeDefinition.Create<UserImport>();
103+
definition.TargetTable = "dbo.User";
104+
definition.IncludeDelete = true;
105+
106+
var sql = DataMergeGenerator.BuildMerge(definition);
107+
108+
sql.Should().Contain("WHEN NOT MATCHED BY SOURCE THEN \r\n DELETE".ReplaceLineEndings());
109+
}
110+
111+
[Fact]
112+
public void BuildMergeNoDeleteWhenIncludeDeleteFalse()
113+
{
114+
var definition = DataMergeDefinition.Create<UserImport>();
115+
definition.TargetTable = "dbo.User";
116+
definition.IncludeDelete = false;
117+
definition.SoftDeleteColumn = "IsDeleted";
118+
definition.SoftDeleteValue = true;
119+
120+
var sql = DataMergeGenerator.BuildMerge(definition);
121+
122+
sql.Should().NotContain("NOT MATCHED BY SOURCE");
123+
}
124+
71125
[Fact]
72126
public async System.Threading.Tasks.Task BuildMergeTests()
73127
{

0 commit comments

Comments
 (0)