From 81cb0df6bf67b6f96a544f6803eeb9f5c01c9694 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 3 Jan 2026 09:13:02 -0500 Subject: [PATCH] feat(etl): implement ColumnRenameTransformer Add transformer for renaming columns in the data stream during ETL. Supports case-insensitive column name matching and multiple renames. --- .../Transformers/ColumnRenameTransformer.cs | 51 ++++++++++++++ .../ColumnRenameTransformerTests.cs | 69 +++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 NEW/src/JdeScoping.DataSync/Etl/Transformers/ColumnRenameTransformer.cs create mode 100644 NEW/tests/JdeScoping.DataSync.Tests/Etl/Transformers/ColumnRenameTransformerTests.cs diff --git a/NEW/src/JdeScoping.DataSync/Etl/Transformers/ColumnRenameTransformer.cs b/NEW/src/JdeScoping.DataSync/Etl/Transformers/ColumnRenameTransformer.cs new file mode 100644 index 0000000..dba8c3d --- /dev/null +++ b/NEW/src/JdeScoping.DataSync/Etl/Transformers/ColumnRenameTransformer.cs @@ -0,0 +1,51 @@ +using System.Data; + +namespace JdeScoping.DataSync.Etl.Transformers; + +/// +/// A data transformer that renames specified columns in the data stream. +/// Columns are matched by name (case-insensitive). +/// +public class ColumnRenameTransformer : DataTransformerBase +{ + private readonly Dictionary _renames; + private string[]? _outputNames; + private Dictionary? _nameToOrdinal; + + /// + public override string TransformerName => $"RenameColumns:{_renames.Count}"; + + /// + /// Creates a new ColumnRenameTransformer that renames the specified columns. + /// + /// Tuples of (OldName, NewName) for columns to rename (case-insensitive matching). + public ColumnRenameTransformer(params (string OldName, string NewName)[] renames) + { + ArgumentNullException.ThrowIfNull(renames); + _renames = renames.ToDictionary(r => r.OldName, r => r.NewName, StringComparer.OrdinalIgnoreCase); + } + + /// + protected override void OnInitialize(IDataReader source) + { + _outputNames = new string[source.FieldCount]; + _nameToOrdinal = new Dictionary(StringComparer.OrdinalIgnoreCase); + for (int i = 0; i < source.FieldCount; i++) + { + var originalName = source.GetName(i); + var outputName = _renames.TryGetValue(originalName, out var newName) ? newName : originalName; + _outputNames[i] = outputName; + _nameToOrdinal[outputName] = i; + } + } + + /// + public override string GetName(int ordinal, IDataReader source) => _outputNames![ordinal]; + + /// + public override int GetOrdinal(string name, IDataReader source) + { + if (_nameToOrdinal!.TryGetValue(name, out var ordinal)) return ordinal; + throw new IndexOutOfRangeException($"Column '{name}' not found."); + } +} diff --git a/NEW/tests/JdeScoping.DataSync.Tests/Etl/Transformers/ColumnRenameTransformerTests.cs b/NEW/tests/JdeScoping.DataSync.Tests/Etl/Transformers/ColumnRenameTransformerTests.cs new file mode 100644 index 0000000..1cee1f6 --- /dev/null +++ b/NEW/tests/JdeScoping.DataSync.Tests/Etl/Transformers/ColumnRenameTransformerTests.cs @@ -0,0 +1,69 @@ +using System.Data; +using JdeScoping.DataSync.Etl.Transformers; +using NSubstitute; + +namespace JdeScoping.DataSync.Tests.Etl.Transformers; + +public class ColumnRenameTransformerTests +{ + [Fact] + public void GetName_ReturnsRenamedColumn() + { + var source = CreateMockReader(new[] { "OldName", "Other" }); + var transformer = new ColumnRenameTransformer(("OldName", "NewName")); + var reader = transformer.Transform(source); + Assert.Equal("NewName", reader.GetName(0)); + Assert.Equal("Other", reader.GetName(1)); + } + + [Fact] + public void GetOrdinal_FindsByNewName() + { + var source = CreateMockReader(new[] { "OldName", "Other" }); + var transformer = new ColumnRenameTransformer(("OldName", "NewName")); + var reader = transformer.Transform(source); + Assert.Equal(0, reader.GetOrdinal("NewName")); + } + + [Fact] + public void GetOrdinal_OldName_ThrowsIndexOutOfRange() + { + var source = CreateMockReader(new[] { "OldName", "Other" }); + var transformer = new ColumnRenameTransformer(("OldName", "NewName")); + var reader = transformer.Transform(source); + Assert.Throws(() => reader.GetOrdinal("OldName")); + } + + [Fact] + public void FieldCount_Unchanged() + { + var source = CreateMockReader(new[] { "OldName", "Other" }); + var transformer = new ColumnRenameTransformer(("OldName", "NewName")); + var reader = transformer.Transform(source); + Assert.Equal(2, reader.FieldCount); + } + + [Fact] + public void MultipleRenames_AllApplied() + { + var source = CreateMockReader(new[] { "A", "B", "C" }); + var transformer = new ColumnRenameTransformer(("A", "Alpha"), ("C", "Charlie")); + var reader = transformer.Transform(source); + Assert.Equal("Alpha", reader.GetName(0)); + Assert.Equal("B", reader.GetName(1)); + Assert.Equal("Charlie", reader.GetName(2)); + } + + private static IDataReader CreateMockReader(string[] columns) + { + var reader = Substitute.For(); + 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); + } + return reader; + } +}