fix(etl): address Codex MCP review findings for Phase 2

- 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
This commit is contained in:
Joseph Doherty
2026-01-03 11:27:07 -05:00
parent fcd8b660fa
commit 7dcbacd5ca
7 changed files with 111 additions and 8 deletions
@@ -98,4 +98,20 @@ public class CommonScriptsTests
Assert.Equal("TimeoutScript", runner.ScriptName);
Assert.NotNull(runner);
}
[Theory]
[InlineData("WorkOrder", "[dbo].[WorkOrder]")]
[InlineData("dbo.WorkOrder", "[dbo].[WorkOrder]")]
[InlineData("[dbo].[WorkOrder]", "[dbo].[WorkOrder]")]
[InlineData("Config.Settings", "[Config].[Settings]")]
[InlineData("[Config].[Settings]", "[Config].[Settings]")]
[InlineData("myschema.MyTable", "[myschema].[MyTable]")]
public void FormatQualifiedTableName_VariousFormats_FormatsCorrectly(string input, string expected)
{
// Act
var result = CommonScripts.FormatQualifiedTableName(input);
// Assert
Assert.Equal(expected, result);
}
}
@@ -240,6 +240,46 @@ public class JdeDateTransformerTests
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>();