304 lines
11 KiB
C#
304 lines
11 KiB
C#
using System.Data;
|
|
using JdeScoping.DataAccess.Interfaces;
|
|
using JdeScoping.DataSync.Etl.Destinations;
|
|
using NSubstitute;
|
|
|
|
namespace JdeScoping.DataSync.Tests.Etl.Destinations;
|
|
|
|
public class DbBulkMergeDestinationTests
|
|
{
|
|
/// <summary>
|
|
/// This test documents that column mapping is applied to ignore extra source columns.
|
|
/// The actual functionality requires a database connection and is an integration test concept.
|
|
/// The implementation fetches destination columns from INFORMATION_SCHEMA.COLUMNS
|
|
/// and only maps columns that exist in both source and destination.
|
|
/// </summary>
|
|
[Fact]
|
|
public void WriteAsync_SourceHasExtraColumns_IgnoresExtraColumns_IntegrationTestConcept()
|
|
{
|
|
// This is an integration test concept -
|
|
// The actual behavior verifies that column mappings are applied:
|
|
// 1. GetDestinationColumnsAsync fetches columns from INFORMATION_SCHEMA.COLUMNS
|
|
// 2. ProcessBatchAsync only adds column mappings for columns in destination
|
|
// 3. Extra source columns are silently ignored during bulk copy
|
|
//
|
|
// To test this fully, an integration test with a real database is required.
|
|
// The unit test here just verifies the component can be constructed.
|
|
var factory = Substitute.For<IDbConnectionFactory>();
|
|
var dest = new DbBulkMergeDestination(factory, "TestTable", new[] { "Id" });
|
|
Assert.NotNull(dest);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_SetsDestinationName()
|
|
{
|
|
var factory = Substitute.For<IDbConnectionFactory>();
|
|
var dest = new DbBulkMergeDestination(factory, "WorkOrder", new[] { "OrderNumber" });
|
|
Assert.Equal("BulkMerge:WorkOrder", dest.DestinationName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_NullFactory_ThrowsArgumentNullException()
|
|
{
|
|
Assert.Throws<ArgumentNullException>(() =>
|
|
new DbBulkMergeDestination(null!, "WorkOrder", new[] { "Id" }));
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_NullTableName_ThrowsArgumentNullException()
|
|
{
|
|
var factory = Substitute.For<IDbConnectionFactory>();
|
|
Assert.Throws<ArgumentNullException>(() =>
|
|
new DbBulkMergeDestination(factory, null!, new[] { "Id" }));
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_EmptyTableName_ThrowsArgumentException()
|
|
{
|
|
var factory = Substitute.For<IDbConnectionFactory>();
|
|
Assert.Throws<ArgumentException>(() =>
|
|
new DbBulkMergeDestination(factory, "", new[] { "Id" }));
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_EmptyMatchColumns_ThrowsArgumentException()
|
|
{
|
|
var factory = Substitute.For<IDbConnectionFactory>();
|
|
Assert.Throws<ArgumentException>(() =>
|
|
new DbBulkMergeDestination(factory, "WorkOrder", Array.Empty<string>()));
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_NullMatchColumns_ThrowsArgumentNullException()
|
|
{
|
|
var factory = Substitute.For<IDbConnectionFactory>();
|
|
Assert.Throws<ArgumentNullException>(() =>
|
|
new DbBulkMergeDestination(factory, "WorkOrder", null!));
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_WithUpdateColumns_Succeeds()
|
|
{
|
|
var factory = Substitute.For<IDbConnectionFactory>();
|
|
var dest = new DbBulkMergeDestination(factory, "WorkOrder",
|
|
new[] { "OrderNumber" },
|
|
updateColumns: new[] { "Status", "Description" });
|
|
Assert.Equal("BulkMerge:WorkOrder", dest.DestinationName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_CustomTimeout_SetsTimeout()
|
|
{
|
|
// Arrange & Act
|
|
var factory = Substitute.For<IDbConnectionFactory>();
|
|
var dest = new DbBulkMergeDestination(
|
|
factory,
|
|
"TestTable",
|
|
new[] { "Id" },
|
|
commandTimeoutSeconds: 1800);
|
|
|
|
// Assert - can't directly test private field, but constructor should accept it
|
|
Assert.NotNull(dest);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_ZeroTimeout_UsesDefault()
|
|
{
|
|
// Arrange & Act
|
|
var factory = Substitute.For<IDbConnectionFactory>();
|
|
var dest = new DbBulkMergeDestination(
|
|
factory,
|
|
"TestTable",
|
|
new[] { "Id" },
|
|
commandTimeoutSeconds: 0);
|
|
|
|
// Assert
|
|
Assert.NotNull(dest);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_WithExcludeFromUpdate_Succeeds()
|
|
{
|
|
// Arrange & Act
|
|
var factory = Substitute.For<IDbConnectionFactory>();
|
|
var dest = new DbBulkMergeDestination(
|
|
factory,
|
|
"WorkOrder",
|
|
new[] { "OrderNumber" },
|
|
excludeFromUpdate: new[] { "CreatedDate", "CreatedBy" });
|
|
|
|
// Assert
|
|
Assert.NotNull(dest);
|
|
Assert.Equal("BulkMerge:WorkOrder", dest.DestinationName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_WithUpdateCondition_Succeeds()
|
|
{
|
|
// Arrange & Act
|
|
var factory = Substitute.For<IDbConnectionFactory>();
|
|
var dest = new DbBulkMergeDestination(
|
|
factory,
|
|
"WorkOrder",
|
|
new[] { "OrderNumber" },
|
|
updateCondition: "source.LastUpdate > target.LastUpdate");
|
|
|
|
// Assert
|
|
Assert.NotNull(dest);
|
|
Assert.Equal("BulkMerge:WorkOrder", dest.DestinationName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_WithAllNewParameters_Succeeds()
|
|
{
|
|
// Arrange & Act
|
|
var factory = Substitute.For<IDbConnectionFactory>();
|
|
var dest = new DbBulkMergeDestination(
|
|
factory,
|
|
"WorkOrder",
|
|
new[] { "OrderNumber" },
|
|
updateColumns: new[] { "Status", "Description" },
|
|
excludeFromUpdate: new[] { "CreatedDate" },
|
|
updateCondition: "source.LastUpdate > target.LastUpdate",
|
|
batchSize: 5000,
|
|
commandTimeoutSeconds: 300);
|
|
|
|
// Assert
|
|
Assert.NotNull(dest);
|
|
Assert.Equal("BulkMerge:WorkOrder", dest.DestinationName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_WithNullExcludeFromUpdate_Succeeds()
|
|
{
|
|
// Arrange & Act
|
|
var factory = Substitute.For<IDbConnectionFactory>();
|
|
var dest = new DbBulkMergeDestination(
|
|
factory,
|
|
"WorkOrder",
|
|
new[] { "OrderNumber" },
|
|
updateColumns: null,
|
|
excludeFromUpdate: null,
|
|
updateCondition: null);
|
|
|
|
// Assert
|
|
Assert.NotNull(dest);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_WithEmptyExcludeFromUpdate_Succeeds()
|
|
{
|
|
// Arrange & Act
|
|
var factory = Substitute.For<IDbConnectionFactory>();
|
|
var dest = new DbBulkMergeDestination(
|
|
factory,
|
|
"WorkOrder",
|
|
new[] { "OrderNumber" },
|
|
excludeFromUpdate: Array.Empty<string>());
|
|
|
|
// Assert
|
|
Assert.NotNull(dest);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_WithEmptyUpdateCondition_Succeeds()
|
|
{
|
|
// Arrange & Act
|
|
var factory = Substitute.For<IDbConnectionFactory>();
|
|
var dest = new DbBulkMergeDestination(
|
|
factory,
|
|
"WorkOrder",
|
|
new[] { "OrderNumber" },
|
|
updateCondition: "");
|
|
|
|
// Assert
|
|
Assert.NotNull(dest);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_WithWhitespaceUpdateCondition_Succeeds()
|
|
{
|
|
// Arrange & Act
|
|
var factory = Substitute.For<IDbConnectionFactory>();
|
|
var dest = new DbBulkMergeDestination(
|
|
factory,
|
|
"WorkOrder",
|
|
new[] { "OrderNumber" },
|
|
updateCondition: " ");
|
|
|
|
// Assert
|
|
Assert.NotNull(dest);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Documents that excludeFromUpdate takes precedence over updateColumns.
|
|
/// If a column is in both updateColumns and excludeFromUpdate, it should be excluded.
|
|
/// This is verified during actual MERGE SQL generation (integration test concept).
|
|
/// </summary>
|
|
[Fact]
|
|
public void Constructor_WithConflictingUpdateAndExclude_Succeeds()
|
|
{
|
|
// Arrange & Act - excludeFromUpdate takes precedence over updateColumns
|
|
var factory = Substitute.For<IDbConnectionFactory>();
|
|
var dest = new DbBulkMergeDestination(
|
|
factory,
|
|
"WorkOrder",
|
|
new[] { "OrderNumber" },
|
|
updateColumns: new[] { "Status", "CreatedDate" },
|
|
excludeFromUpdate: new[] { "CreatedDate" });
|
|
|
|
// Assert - construction succeeds; actual exclusion behavior is in MERGE SQL generation
|
|
Assert.NotNull(dest);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Documents that updateCondition is applied to the WHEN MATCHED clause.
|
|
/// This adds conditional update logic like "source.LastUpdate > target.LastUpdate"
|
|
/// to prevent stale data from overwriting newer data.
|
|
/// </summary>
|
|
[Fact]
|
|
public void UpdateCondition_DocumentsUsage_IntegrationTestConcept()
|
|
{
|
|
// The updateCondition parameter adds a condition to the MERGE statement's WHEN MATCHED clause.
|
|
// Example: WHEN MATCHED AND source.LastUpdate > target.LastUpdate THEN UPDATE SET ...
|
|
//
|
|
// This is useful for:
|
|
// - Preventing stale data from overwriting newer data
|
|
// - Only updating rows that have actually changed
|
|
// - Implementing optimistic concurrency patterns
|
|
//
|
|
// Full verification requires an integration test with a real database.
|
|
var factory = Substitute.For<IDbConnectionFactory>();
|
|
var dest = new DbBulkMergeDestination(
|
|
factory,
|
|
"WorkOrder",
|
|
new[] { "OrderNumber" },
|
|
updateCondition: "source.LastUpdate > target.LastUpdate");
|
|
Assert.NotNull(dest);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Documents that excludeFromUpdate removes columns from the UPDATE SET clause.
|
|
/// This is useful for columns that should only be set on INSERT (e.g., CreatedDate).
|
|
/// </summary>
|
|
[Fact]
|
|
public void ExcludeFromUpdate_DocumentsUsage_IntegrationTestConcept()
|
|
{
|
|
// The excludeFromUpdate parameter removes specified columns from the UPDATE SET clause.
|
|
// These columns are still included in the INSERT clause for new rows.
|
|
//
|
|
// Common use cases:
|
|
// - CreatedDate, CreatedBy - set only on insert, never updated
|
|
// - Audit columns that should preserve original values
|
|
//
|
|
// Full verification requires an integration test with a real database.
|
|
var factory = Substitute.For<IDbConnectionFactory>();
|
|
var dest = new DbBulkMergeDestination(
|
|
factory,
|
|
"WorkOrder",
|
|
new[] { "OrderNumber" },
|
|
excludeFromUpdate: new[] { "CreatedDate", "CreatedBy" });
|
|
Assert.NotNull(dest);
|
|
}
|
|
}
|