7dcbacd5ca
- Filter MERGE SQL columns to only include columns that exist in destination (allColumns and updateColumns were using unfiltered source columns) - Fix schema-qualified table names to use proper [schema].[table] format instead of wrapping entire name in single brackets - Add empty column mapping validation to throw early if no columns intersect - Add JdeDateTransformer output column collision detection in OnInitialize - Add TODO comment for WithCommandTimeout (stored but not yet passed to destinations) - Add tests for FormatQualifiedTableName and output column collision
299 lines
11 KiB
C#
299 lines
11 KiB
C#
using System.Data;
|
|
using JdeScoping.DataSync.Etl.Transformers;
|
|
using NSubstitute;
|
|
|
|
namespace JdeScoping.DataSync.Tests.Etl.Transformers;
|
|
|
|
public class JdeDateTransformerTests
|
|
{
|
|
[Fact]
|
|
public void FieldCount_ReducedByOne()
|
|
{
|
|
var source = CreateMockReader(new[] { "Id", "UPMJ", "TDAY", "Name" }, new object[] { 1, 124001m, 120000m, "Test" });
|
|
var transformer = new JdeDateTransformer("UPMJ", "TDAY", "UpdatedAt");
|
|
var reader = transformer.Transform(source);
|
|
Assert.Equal(3, reader.FieldCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetName_DateColumnRenamed_TimeColumnRemoved()
|
|
{
|
|
var source = CreateMockReader(new[] { "Id", "UPMJ", "TDAY", "Name" }, new object[] { 1, 124001m, 120000m, "Test" });
|
|
var transformer = new JdeDateTransformer("UPMJ", "TDAY", "UpdatedAt");
|
|
var reader = transformer.Transform(source);
|
|
Assert.Equal("Id", reader.GetName(0));
|
|
Assert.Equal("UpdatedAt", reader.GetName(1));
|
|
Assert.Equal("Name", reader.GetName(2));
|
|
}
|
|
|
|
[Fact]
|
|
public void GetValue_ParsesJulianDateAndTime()
|
|
{
|
|
// Julian date 124001 = Jan 1, 2024 (century digit 1 = 2000s, year 24, day 001)
|
|
// Time 120000 = 12:00:00
|
|
var source = CreateMockReader(new[] { "Id", "UPMJ", "TDAY", "Name" }, new object[] { 1, 124001m, 120000m, "Test" });
|
|
source.Read().Returns(true);
|
|
var transformer = new JdeDateTransformer("UPMJ", "TDAY", "UpdatedAt");
|
|
var reader = transformer.Transform(source);
|
|
reader.Read();
|
|
var expectedDate = new DateTime(2024, 1, 1, 12, 0, 0);
|
|
Assert.Equal(expectedDate, reader.GetValue(1));
|
|
}
|
|
|
|
[Fact]
|
|
public void GetValue_NullDate_ReturnsDbNull()
|
|
{
|
|
var source = CreateMockReader(new[] { "Id", "UPMJ", "TDAY", "Name" }, new object[] { 1, DBNull.Value, DBNull.Value, "Test" });
|
|
source.Read().Returns(true);
|
|
source.IsDBNull(1).Returns(true);
|
|
source.IsDBNull(2).Returns(true);
|
|
var transformer = new JdeDateTransformer("UPMJ", "TDAY", "UpdatedAt");
|
|
var reader = transformer.Transform(source);
|
|
reader.Read();
|
|
Assert.Equal(DBNull.Value, reader.GetValue(1));
|
|
}
|
|
|
|
[Fact]
|
|
public void GetFieldType_DateColumn_ReturnsDateTime()
|
|
{
|
|
var source = CreateMockReader(new[] { "Id", "UPMJ", "TDAY", "Name" }, new object[] { 1, 124001m, 120000m, "Test" });
|
|
var transformer = new JdeDateTransformer("UPMJ", "TDAY", "UpdatedAt");
|
|
var reader = transformer.Transform(source);
|
|
Assert.Equal(typeof(DateTime), reader.GetFieldType(1));
|
|
}
|
|
|
|
[Fact]
|
|
public void GetOrdinal_NewDateColumn_ReturnsCorrectOrdinal()
|
|
{
|
|
var source = CreateMockReader(new[] { "Id", "UPMJ", "TDAY", "Name" }, new object[] { 1, 124001m, 120000m, "Test" });
|
|
var transformer = new JdeDateTransformer("UPMJ", "TDAY", "UpdatedAt");
|
|
var reader = transformer.Transform(source);
|
|
Assert.Equal(1, reader.GetOrdinal("UpdatedAt"));
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseJdeDateTime_VariousDates()
|
|
{
|
|
// Test the static parsing method
|
|
Assert.Equal(new DateTime(2024, 1, 1, 12, 0, 0), JdeDateTransformer.ParseJdeDateTime(124001m, 120000m));
|
|
Assert.Equal(new DateTime(2023, 12, 31, 23, 59, 59), JdeDateTransformer.ParseJdeDateTime(123365m, 235959m));
|
|
Assert.Equal(new DateTime(1999, 6, 15, 0, 0, 0), JdeDateTransformer.ParseJdeDateTime(99166m, 0m));
|
|
}
|
|
|
|
[Fact]
|
|
public void MapOrdinal_DateOutputColumn_ReturnsNegativeOne()
|
|
{
|
|
// Arrange
|
|
var transformer = new JdeDateTransformer("UPMJ", "TDAY", "UpdatedAt");
|
|
var source = CreateMockReader(new[] { "UPMJ", "TDAY", "Other" }, new object[] { 124001m, 120000m, "Test" });
|
|
transformer.Transform(source);
|
|
|
|
// Act - ordinal 0 is the computed DateTime column (UPMJ becomes UpdatedAt at position 0)
|
|
var result = transformer.MapOrdinal(0, source);
|
|
|
|
// Assert - computed columns return -1
|
|
Assert.Equal(-1, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void MapOrdinal_NonComputedColumn_ReturnsSourceOrdinal()
|
|
{
|
|
// Arrange
|
|
var transformer = new JdeDateTransformer("UPMJ", "TDAY", "UpdatedAt");
|
|
var source = CreateMockReader(new[] { "UPMJ", "TDAY", "Other" }, new object[] { 124001m, 120000m, "Test" });
|
|
transformer.Transform(source);
|
|
|
|
// Act - ordinal 1 is "Other" which maps to source ordinal 2
|
|
var result = transformer.MapOrdinal(1, source);
|
|
|
|
// Assert
|
|
Assert.Equal(2, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseJdeDateTime_InvalidDate_ReturnsSentinel()
|
|
{
|
|
// Arrange
|
|
var sentinel = new DateTime(1900, 1, 1);
|
|
|
|
// Act - 999999 is invalid (century 9 doesn't exist)
|
|
var result = JdeDateTransformer.ParseJdeDateTime(999999m, 0m, sentinel);
|
|
|
|
// Assert
|
|
Assert.Equal(sentinel, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseJdeDateTime_ZeroDate_ReturnsSentinel()
|
|
{
|
|
// Arrange
|
|
var sentinel = new DateTime(1900, 1, 1);
|
|
|
|
// Act
|
|
var result = JdeDateTransformer.ParseJdeDateTime(0m, 0m, sentinel);
|
|
|
|
// Assert
|
|
Assert.Equal(sentinel, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void DefaultInvalidDateSentinel_Is1900()
|
|
{
|
|
// Assert
|
|
Assert.Equal(new DateTime(1900, 1, 1), JdeDateTransformer.DefaultInvalidDateSentinel);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetDataTypeName_DateOutputColumn_ReturnsDatetime()
|
|
{
|
|
// Arrange
|
|
var transformer = new JdeDateTransformer("UPMJ", "TDAY", "UpdatedAt");
|
|
var source = CreateMockReader(new[] { "UPMJ", "TDAY", "Other" }, new object[] { 124001m, 120000m, "Test" });
|
|
source.GetDataTypeName(2).Returns("nvarchar");
|
|
var reader = transformer.Transform(source);
|
|
|
|
// Act - ordinal 0 is the computed DateTime column
|
|
var result = reader.GetDataTypeName(0);
|
|
|
|
// Assert
|
|
Assert.Equal("datetime", result);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetDataTypeName_NonComputedColumn_DelegatesToSource()
|
|
{
|
|
// Arrange
|
|
var transformer = new JdeDateTransformer("UPMJ", "TDAY", "UpdatedAt");
|
|
var source = CreateMockReader(new[] { "UPMJ", "TDAY", "Other" }, new object[] { 124001m, 120000m, "Test" });
|
|
source.GetDataTypeName(2).Returns("nvarchar");
|
|
var reader = transformer.Transform(source);
|
|
|
|
// Act - ordinal 1 is "Other" which maps to source ordinal 2
|
|
var result = reader.GetDataTypeName(1);
|
|
|
|
// Assert
|
|
Assert.Equal("nvarchar", result);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseJdeDateTime_NegativeDate_ReturnsSentinel()
|
|
{
|
|
// Arrange
|
|
var sentinel = new DateTime(1900, 1, 1);
|
|
|
|
// Act
|
|
var result = JdeDateTransformer.ParseJdeDateTime(-100m, 0m, sentinel);
|
|
|
|
// Assert
|
|
Assert.Equal(sentinel, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseJdeDateTime_InvalidDayOfYear_ReturnsSentinel()
|
|
{
|
|
// Arrange
|
|
var sentinel = new DateTime(1900, 1, 1);
|
|
|
|
// Act - Day 400 doesn't exist
|
|
var result = JdeDateTransformer.ParseJdeDateTime(124400m, 0m, sentinel);
|
|
|
|
// Assert
|
|
Assert.Equal(sentinel, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseJdeDateTime_InvalidTime_ReturnsSentinel()
|
|
{
|
|
// Arrange
|
|
var sentinel = new DateTime(1900, 1, 1);
|
|
|
|
// Act - Hour 25 doesn't exist
|
|
var result = JdeDateTransformer.ParseJdeDateTime(124001m, 250000m, sentinel);
|
|
|
|
// Assert
|
|
Assert.Equal(sentinel, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseJdeDateTime_LeapYearDay366_ReturnsValidDate()
|
|
{
|
|
// Arrange
|
|
var sentinel = new DateTime(1900, 1, 1);
|
|
|
|
// Act - 2024 is a leap year, day 366 is valid
|
|
var result = JdeDateTransformer.ParseJdeDateTime(124366m, 0m, sentinel);
|
|
|
|
// Assert - December 31, 2024
|
|
Assert.Equal(new DateTime(2024, 12, 31), result);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseJdeDateTime_NonLeapYearDay366_ReturnsSentinel()
|
|
{
|
|
// Arrange
|
|
var sentinel = new DateTime(1900, 1, 1);
|
|
|
|
// Act - 2023 is NOT a leap year, day 366 is invalid
|
|
var result = JdeDateTransformer.ParseJdeDateTime(123366m, 0m, sentinel);
|
|
|
|
// Assert
|
|
Assert.Equal(sentinel, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void OutputColumnConflictsWithExistingColumn_ThrowsInvalidOperationException()
|
|
{
|
|
// Arrange - "Name" column exists and we try to use it as output
|
|
var source = CreateMockReader(new[] { "Id", "UPMJ", "TDAY", "Name" }, new object[] { 1, 124001m, 120000m, "Test" });
|
|
var transformer = new JdeDateTransformer("UPMJ", "TDAY", "Name"); // "Name" conflicts
|
|
|
|
// Act & Assert
|
|
var ex = Assert.Throws<InvalidOperationException>(() => transformer.Transform(source));
|
|
Assert.Contains("conflicts with existing column", ex.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public void OutputColumnMatchesDateColumn_Succeeds()
|
|
{
|
|
// Arrange - output column can match the date column name (it replaces it)
|
|
var source = CreateMockReader(new[] { "Id", "UPMJ", "TDAY", "Name" }, new object[] { 1, 124001m, 120000m, "Test" });
|
|
var transformer = new JdeDateTransformer("UPMJ", "TDAY", "UPMJ"); // Same as date column
|
|
|
|
// Act - should not throw
|
|
var reader = transformer.Transform(source);
|
|
|
|
// Assert
|
|
Assert.Equal("UPMJ", reader.GetName(1)); // Still named UPMJ
|
|
}
|
|
|
|
[Fact]
|
|
public void OutputColumnMatchesTimeColumn_Succeeds()
|
|
{
|
|
// Arrange - output column can match the time column name (it's removed anyway)
|
|
var source = CreateMockReader(new[] { "Id", "UPMJ", "TDAY", "Name" }, new object[] { 1, 124001m, 120000m, "Test" });
|
|
var transformer = new JdeDateTransformer("UPMJ", "TDAY", "TDAY"); // Same as time column
|
|
|
|
// Act - should not throw
|
|
var reader = transformer.Transform(source);
|
|
|
|
// Assert
|
|
Assert.Equal(3, reader.FieldCount); // One column removed
|
|
}
|
|
|
|
private static IDataReader CreateMockReader(string[] columns, object[] values)
|
|
{
|
|
var reader = Substitute.For<IDataReader>();
|
|
reader.FieldCount.Returns(columns.Length);
|
|
for (int i = 0; i < columns.Length; i++)
|
|
{
|
|
var index = i;
|
|
reader.GetName(index).Returns(columns[index]);
|
|
reader.GetOrdinal(columns[index]).Returns(index);
|
|
reader.GetValue(index).Returns(values[index]);
|
|
reader.IsDBNull(index).Returns(values[index] == DBNull.Value);
|
|
reader.GetFieldType(index).Returns(values[index]?.GetType() ?? typeof(object));
|
|
}
|
|
return reader;
|
|
}
|
|
}
|