# TableSyncOperation Migration Design ## Goal Migrate `TableSyncOperation` from using `IStagingTableManager` with `TableSpec` metadata to using `IBulkMergeHelper` with expression-based `IMergeConfiguration` classes. Remove old staging table code after migration. ## Architecture Decisions | Decision | Choice | Rationale | |----------|--------|-----------| | Column metadata source | Expression lambdas | Compile-time safety, explicit configuration | | Configuration style | Fluent `IMergeConfiguration` classes | Clean Architecture - keeps domain entities clean | | Configuration location | DataSync project | Infrastructure concern, not domain | | Mass update support | Add to `BulkMergeHelper` | Unified bulk operations API | | Index management | Built into `BulkMergeHelper` | Simple API, matches current behavior | | DataReader generation | Source generator (fix first) | Best long-term solution, block until working | | Old code removal | Full removal | Clean break, no dead code | | DI registration | Explicit per-entity | Visibility and control | --- ## Component Design ### 1. IMergeConfiguration Interface Location: `src/JdeScoping.DataSync/Contracts/IMergeConfiguration.cs` ```csharp public interface IMergeConfiguration where T : class { /// Table name in SQL Server cache. string TableName { get; } /// Columns to match on (primary key). Expression> MatchOn { get; } /// Columns to update when matched. Null = all non-PK columns. Expression>? UpdateColumns { get; } /// Condition for when to update. Null = always update on match. Expression>? UpdateWhen { get; } /// Columns to insert. Null = all columns. Expression>? InsertColumns { get; } } ``` ### 2. Example Merge Configuration Location: `src/JdeScoping.DataSync/Configuration/MergeConfigurations/WorkOrderMergeConfiguration.cs` ```csharp public class WorkOrderMergeConfiguration : IMergeConfiguration { public string TableName => "WorkOrder"; public Expression> MatchOn => x => new { x.WorkOrderNumber, x.BranchCode }; public Expression>? UpdateColumns => x => new { x.Status, x.Quantity, x.LastUpdateDt }; public Expression>? UpdateWhen => (src, tgt) => src.LastUpdateDt > tgt.LastUpdateDt; public Expression>? InsertColumns => null; // All columns } ``` ### 3. Extended IBulkMergeHelper Added to: `src/JdeScoping.DataSync/Contracts/IBulkMergeHelper.cs` ```csharp /// /// Performs a mass insert (full table refresh) with optional index management. /// Truncates table, disables non-clustered indexes, bulk copies data, rebuilds indexes. /// Task MassInsertAsync( IAsyncEnumerable data, string destinationTable, bool rebuildIndexes = true, int batchSize = 0, CancellationToken cancellationToken = default) where T : class; ``` New result type: ```csharp public record MassInsertResult( long TotalRowsInserted, TimeSpan Elapsed, bool IndexesRebuilt); ``` ### 4. MergeConfigurationRegistry Location: `src/JdeScoping.DataSync/Services/MergeConfigurationRegistry.cs` ```csharp public interface IMergeConfigurationRegistry { IMergeConfiguration GetConfiguration() where T : class; bool HasConfiguration() where T : class; } internal class MergeConfigurationRegistry : IMergeConfigurationRegistry { private readonly IServiceProvider _serviceProvider; public MergeConfigurationRegistry(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public IMergeConfiguration GetConfiguration() where T : class { return _serviceProvider.GetService>() ?? throw new InvalidOperationException( $"No merge configuration registered for {typeof(T).Name}"); } public bool HasConfiguration() where T : class { return _serviceProvider.GetService>() != null; } } ``` ### 5. Refactored TableSyncOperation Key changes to `src/JdeScoping.DataSync/Services/TableSyncOperation.cs`: - Remove `IStagingTableManager` dependency - Add `IBulkMergeHelper` dependency - Add `IMergeConfigurationRegistry` dependency - Remove `ILotFinderRepository` dependency (no longer need `GetTableSpecAsync`) ```csharp private async Task ExecuteIncrementalUpdateAsync( IAsyncEnumerable data, DataUpdateTask task, CancellationToken ct) where T : class { var config = _configRegistry.GetConfiguration(); var result = await _bulkMergeHelper.MergeAsync( data, config.TableName, config.MatchOn, config.UpdateColumns, config.UpdateWhen, config.InsertColumns, batchSize: _options.Value.BatchSize, cancellationToken: ct); return result.TotalRowsProcessed; } private async Task ExecuteMassUpdateAsync( IAsyncEnumerable data, DataUpdateTask task, CancellationToken ct) where T : class { var config = _configRegistry.GetConfiguration(); var result = await _bulkMergeHelper.MassInsertAsync( data, config.TableName, rebuildIndexes: task.ScheduleConfig.ReIndexData, batchSize: _options.Value.BulkCopyBatchSize, cancellationToken: ct); return result.TotalRowsInserted; } ``` ### 6. DI Registration In `ServiceCollectionExtensions.cs`: ```csharp // Merge configuration registry services.AddSingleton(); // Merge configurations - explicit registration services.AddSingleton, WorkOrderMergeConfiguration>(); services.AddSingleton, LotUsageMergeConfiguration>(); services.AddSingleton, ItemMergeConfiguration>(); // ... one line per synced entity ``` --- ## Source Generator Fix The generator at `JdeScoping.DataSync.SourceGenerators` must produce `IDataReader` implementations. **Verification steps:** 1. Confirm `.csproj` reference has `OutputItemType="Analyzer"` and `ReferenceOutputAssembly="false"` 2. Add diagnostic logging to generator 3. Ensure `[GenerateDataReader]` attribute is accessible 4. Generator emits `{EntityName}DataReader : IDataReader` per attributed entity **Generated output pattern:** ```csharp public sealed class WorkOrderDataReader : IDataReader { private readonly IAsyncEnumerator _enumerator; private WorkOrder? _current; private static readonly string[] ColumnNames = ["WorkOrderNumber", "BranchCode", "Status", "Quantity", "LastUpdateDt"]; public object GetValue(int i) => i switch { 0 => _current!.WorkOrderNumber, 1 => _current!.BranchCode, // ... }; } ``` --- ## Code Removal (After Migration Verified) **Delete:** - `src/JdeScoping.DataSync/Contracts/IStagingTableManager.cs` - `src/JdeScoping.DataSync/Services/StagingTableManager.cs` - `tests/JdeScoping.DataSync.Tests/StagingTableManagerTests.cs` - `tests/JdeScoping.DataSync.IntegrationTests/StagingTableManagerTests.cs` **Update:** - Remove `IStagingTableManager`/`StagingTableManager` from `ServiceCollectionExtensions.cs` - Remove `GetTableSpecAsync` usage from DataSync (if no longer needed elsewhere) --- ## Testing Strategy **Unit tests - new:** - `MergeConfigurationRegistryTests` - `BulkMergeHelper.MassInsertAsync` tests **Unit tests - update:** - `TableSyncOperationTests` - update mocks **Integration tests:** - `BulkMergeHelperTests` - add mass insert tests - `TableSyncOperationTests` - end-to-end verification **Source generator tests:** - Verify generated `IDataReader` implementations - Test with `SqlBulkCopy` **Verification before old code removal:** 1. All existing tests pass 2. Integration tests verify same behavior 3. Manual smoke test if possible