From 81b07ce027eabba3314beb9aba2f58239469151c Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 6 Jan 2026 10:18:09 -0500 Subject: [PATCH] feat: extract DevEtl to JdeScoping.DataSync.Dev project - Create JdeScoping.DataSync.Dev for sandbox testing ETL code - Create JdeScoping.DataSync.Dev.Tests for associated tests - Move 22 source files and 8 test files - Update namespaces from DevEtl to Dev - Add both projects to solution --- NEW/JdeScoping.slnx | 2 + .../BranchDevEtl.cs | 2 +- .../JdeScoping.DataSync.Dev/DevEtlRegistry.cs | 188 +++++++++ .../FunctionCodeDevEtl.cs | 38 ++ NEW/src/JdeScoping.DataSync.Dev/ItemDevEtl.cs | 41 ++ .../JdeScoping.DataSync.Dev.csproj | 13 + .../JdeScoping.DataSync.Dev/JdeUserDevEtl.cs | 39 ++ NEW/src/JdeScoping.DataSync.Dev/LotDevEtl.cs | 45 ++ .../LotUsageCurrDevEtl.cs | 42 ++ .../LotUsageHistDevEtl.cs | 42 ++ .../JdeScoping.DataSync.Dev/MisDataDevEtl.cs | 49 +++ .../OrgHierarchyDevEtl.cs | 39 ++ .../ProfitCenterDevEtl.cs | 38 ++ .../RouteMasterDevEtl.cs | 44 ++ .../WorkCenterDevEtl.cs | 38 ++ .../WorkOrderComponentCurrDevEtl.cs | 42 ++ .../WorkOrderComponentHistDevEtl.cs | 42 ++ .../WorkOrderCurrDevEtl.cs | 50 +++ .../WorkOrderHistDevEtl.cs | 50 +++ .../WorkOrderRoutingDevEtl.cs | 48 +++ .../WorkOrderStepCurrDevEtl.cs | 46 +++ .../WorkOrderStepHistDevEtl.cs | 46 +++ .../WorkOrderTimeCurrDevEtl.cs | 43 ++ .../WorkOrderTimeHistDevEtl.cs | 43 ++ .../DevEtl/DevEtlRegistry.cs | 80 ---- .../BranchDevEtlTests.cs | 4 +- .../FunctionCodeDevEtlTests.cs | 96 +++++ .../ItemDevEtlTests.cs | 96 +++++ .../JdeScoping.DataSync.Dev.Tests.csproj | 44 ++ .../JdeUserDevEtlTests.cs | 96 +++++ .../OrgHierarchyDevEtlTests.cs | 166 ++++++++ .../ProfitCenterDevEtlTests.cs | 117 ++++++ .../RouteMasterDevEtlTests.cs | 96 +++++ .../WorkCenterDevEtlTests.cs | 117 ++++++ .../appsettings.json | 8 + ...26-01-06-datasync-dev-extraction-design.md | 162 ++++++++ PLANS/2026-01-06-datasync-dev-extraction.md | 389 ++++++++++++++++++ 37 files changed, 2458 insertions(+), 83 deletions(-) rename NEW/src/{JdeScoping.DataSync/DevEtl => JdeScoping.DataSync.Dev}/BranchDevEtl.cs (94%) create mode 100644 NEW/src/JdeScoping.DataSync.Dev/DevEtlRegistry.cs create mode 100644 NEW/src/JdeScoping.DataSync.Dev/FunctionCodeDevEtl.cs create mode 100644 NEW/src/JdeScoping.DataSync.Dev/ItemDevEtl.cs create mode 100644 NEW/src/JdeScoping.DataSync.Dev/JdeScoping.DataSync.Dev.csproj create mode 100644 NEW/src/JdeScoping.DataSync.Dev/JdeUserDevEtl.cs create mode 100644 NEW/src/JdeScoping.DataSync.Dev/LotDevEtl.cs create mode 100644 NEW/src/JdeScoping.DataSync.Dev/LotUsageCurrDevEtl.cs create mode 100644 NEW/src/JdeScoping.DataSync.Dev/LotUsageHistDevEtl.cs create mode 100644 NEW/src/JdeScoping.DataSync.Dev/MisDataDevEtl.cs create mode 100644 NEW/src/JdeScoping.DataSync.Dev/OrgHierarchyDevEtl.cs create mode 100644 NEW/src/JdeScoping.DataSync.Dev/ProfitCenterDevEtl.cs create mode 100644 NEW/src/JdeScoping.DataSync.Dev/RouteMasterDevEtl.cs create mode 100644 NEW/src/JdeScoping.DataSync.Dev/WorkCenterDevEtl.cs create mode 100644 NEW/src/JdeScoping.DataSync.Dev/WorkOrderComponentCurrDevEtl.cs create mode 100644 NEW/src/JdeScoping.DataSync.Dev/WorkOrderComponentHistDevEtl.cs create mode 100644 NEW/src/JdeScoping.DataSync.Dev/WorkOrderCurrDevEtl.cs create mode 100644 NEW/src/JdeScoping.DataSync.Dev/WorkOrderHistDevEtl.cs create mode 100644 NEW/src/JdeScoping.DataSync.Dev/WorkOrderRoutingDevEtl.cs create mode 100644 NEW/src/JdeScoping.DataSync.Dev/WorkOrderStepCurrDevEtl.cs create mode 100644 NEW/src/JdeScoping.DataSync.Dev/WorkOrderStepHistDevEtl.cs create mode 100644 NEW/src/JdeScoping.DataSync.Dev/WorkOrderTimeCurrDevEtl.cs create mode 100644 NEW/src/JdeScoping.DataSync.Dev/WorkOrderTimeHistDevEtl.cs delete mode 100644 NEW/src/JdeScoping.DataSync/DevEtl/DevEtlRegistry.cs rename NEW/tests/{JdeScoping.DataSync.Tests/DevEtl => JdeScoping.DataSync.Dev.Tests}/BranchDevEtlTests.cs (95%) create mode 100644 NEW/tests/JdeScoping.DataSync.Dev.Tests/FunctionCodeDevEtlTests.cs create mode 100644 NEW/tests/JdeScoping.DataSync.Dev.Tests/ItemDevEtlTests.cs create mode 100644 NEW/tests/JdeScoping.DataSync.Dev.Tests/JdeScoping.DataSync.Dev.Tests.csproj create mode 100644 NEW/tests/JdeScoping.DataSync.Dev.Tests/JdeUserDevEtlTests.cs create mode 100644 NEW/tests/JdeScoping.DataSync.Dev.Tests/OrgHierarchyDevEtlTests.cs create mode 100644 NEW/tests/JdeScoping.DataSync.Dev.Tests/ProfitCenterDevEtlTests.cs create mode 100644 NEW/tests/JdeScoping.DataSync.Dev.Tests/RouteMasterDevEtlTests.cs create mode 100644 NEW/tests/JdeScoping.DataSync.Dev.Tests/WorkCenterDevEtlTests.cs create mode 100644 NEW/tests/JdeScoping.DataSync.Dev.Tests/appsettings.json create mode 100644 PLANS/2026-01-06-datasync-dev-extraction-design.md create mode 100644 PLANS/2026-01-06-datasync-dev-extraction.md diff --git a/NEW/JdeScoping.slnx b/NEW/JdeScoping.slnx index f10b23c..519e1d6 100644 --- a/NEW/JdeScoping.slnx +++ b/NEW/JdeScoping.slnx @@ -7,6 +7,7 @@ + @@ -20,6 +21,7 @@ + diff --git a/NEW/src/JdeScoping.DataSync/DevEtl/BranchDevEtl.cs b/NEW/src/JdeScoping.DataSync.Dev/BranchDevEtl.cs similarity index 94% rename from NEW/src/JdeScoping.DataSync/DevEtl/BranchDevEtl.cs rename to NEW/src/JdeScoping.DataSync.Dev/BranchDevEtl.cs index ac83fff..584de0e 100644 --- a/NEW/src/JdeScoping.DataSync/DevEtl/BranchDevEtl.cs +++ b/NEW/src/JdeScoping.DataSync.Dev/BranchDevEtl.cs @@ -4,7 +4,7 @@ using JdeScoping.DataSync.Etl.Models; using JdeScoping.DataSync.Etl.Pipeline; using JdeScoping.DataSync.Etl.Sources; -namespace JdeScoping.DataSync.DevEtl; +namespace JdeScoping.DataSync.Dev; /// /// Development ETL pipeline for the Branch table. diff --git a/NEW/src/JdeScoping.DataSync.Dev/DevEtlRegistry.cs b/NEW/src/JdeScoping.DataSync.Dev/DevEtlRegistry.cs new file mode 100644 index 0000000..207b1de --- /dev/null +++ b/NEW/src/JdeScoping.DataSync.Dev/DevEtlRegistry.cs @@ -0,0 +1,188 @@ +using System.Collections.Concurrent; +using JdeScoping.DataAccess.Interfaces; +using JdeScoping.DataSync.Etl.Pipeline; +using JdeScoping.DataSync.Etl.Results; +using Microsoft.Extensions.Logging; + +namespace JdeScoping.DataSync.Dev; + +/// +/// Registry for development ETL pipelines that load from cached JSON files. +/// +public class DevEtlRegistry +{ + private readonly IDbConnectionFactory _connectionFactory; + private readonly string _cacheDirectory; + private readonly ILogger? _logger; + + private readonly Dictionary> _pipelineFactories = new(StringComparer.OrdinalIgnoreCase) + { + // Small tables (< 1 MB) + [BranchDevEtl.TableName] = (factory, cacheDir) => + BranchDevEtl.Create(factory, Path.Combine(cacheDir, BranchDevEtl.CacheFileName)), + [OrgHierarchyDevEtl.TableName] = (factory, cacheDir) => + OrgHierarchyDevEtl.Create(factory, Path.Combine(cacheDir, OrgHierarchyDevEtl.CacheFileName)), + [WorkCenterDevEtl.TableName] = (factory, cacheDir) => + WorkCenterDevEtl.Create(factory, Path.Combine(cacheDir, WorkCenterDevEtl.CacheFileName)), + [ProfitCenterDevEtl.TableName] = (factory, cacheDir) => + ProfitCenterDevEtl.Create(factory, Path.Combine(cacheDir, ProfitCenterDevEtl.CacheFileName)), + // Medium tables (1-20 MB) + [JdeUserDevEtl.TableName] = (factory, cacheDir) => + JdeUserDevEtl.Create(factory, Path.Combine(cacheDir, JdeUserDevEtl.CacheFileName)), + [FunctionCodeDevEtl.TableName] = (factory, cacheDir) => + FunctionCodeDevEtl.Create(factory, Path.Combine(cacheDir, FunctionCodeDevEtl.CacheFileName)), + [ItemDevEtl.TableName] = (factory, cacheDir) => + ItemDevEtl.Create(factory, Path.Combine(cacheDir, ItemDevEtl.CacheFileName)), + [RouteMasterDevEtl.TableName] = (factory, cacheDir) => + RouteMasterDevEtl.Create(factory, Path.Combine(cacheDir, RouteMasterDevEtl.CacheFileName)), + // Large tables (20-200 MB) + [LotDevEtl.TableName] = (factory, cacheDir) => + LotDevEtl.Create(factory, Path.Combine(cacheDir, LotDevEtl.CacheFileName)), + [MisDataDevEtl.TableName] = (factory, cacheDir) => + MisDataDevEtl.Create(factory, Path.Combine(cacheDir, MisDataDevEtl.CacheFileName)), + [WorkOrderCurrDevEtl.TableName] = (factory, cacheDir) => + WorkOrderCurrDevEtl.Create(factory, Path.Combine(cacheDir, WorkOrderCurrDevEtl.CacheFileName)), + [WorkOrderHistDevEtl.TableName] = (factory, cacheDir) => + WorkOrderHistDevEtl.Create(factory, Path.Combine(cacheDir, WorkOrderHistDevEtl.CacheFileName)), + [LotUsageHistDevEtl.TableName] = (factory, cacheDir) => + LotUsageHistDevEtl.Create(factory, Path.Combine(cacheDir, LotUsageHistDevEtl.CacheFileName)), + [WorkOrderComponentHistDevEtl.TableName] = (factory, cacheDir) => + WorkOrderComponentHistDevEtl.Create(factory, Path.Combine(cacheDir, WorkOrderComponentHistDevEtl.CacheFileName)), + // Very large tables (200+ MB) + [WorkOrderStepHistDevEtl.TableName] = (factory, cacheDir) => + WorkOrderStepHistDevEtl.Create(factory, Path.Combine(cacheDir, WorkOrderStepHistDevEtl.CacheFileName)), + [WorkOrderComponentCurrDevEtl.TableName] = (factory, cacheDir) => + WorkOrderComponentCurrDevEtl.Create(factory, Path.Combine(cacheDir, WorkOrderComponentCurrDevEtl.CacheFileName)), + [WorkOrderRoutingDevEtl.TableName] = (factory, cacheDir) => + WorkOrderRoutingDevEtl.Create(factory, Path.Combine(cacheDir, WorkOrderRoutingDevEtl.CacheFileName)), + [LotUsageCurrDevEtl.TableName] = (factory, cacheDir) => + LotUsageCurrDevEtl.Create(factory, Path.Combine(cacheDir, LotUsageCurrDevEtl.CacheFileName)), + [WorkOrderStepCurrDevEtl.TableName] = (factory, cacheDir) => + WorkOrderStepCurrDevEtl.Create(factory, Path.Combine(cacheDir, WorkOrderStepCurrDevEtl.CacheFileName)), + [WorkOrderTimeHistDevEtl.TableName] = (factory, cacheDir) => + WorkOrderTimeHistDevEtl.Create(factory, Path.Combine(cacheDir, WorkOrderTimeHistDevEtl.CacheFileName)), + [WorkOrderTimeCurrDevEtl.TableName] = (factory, cacheDir) => + WorkOrderTimeCurrDevEtl.Create(factory, Path.Combine(cacheDir, WorkOrderTimeCurrDevEtl.CacheFileName)), + }; + + public DevEtlRegistry( + IDbConnectionFactory connectionFactory, + string cacheDirectory, + ILogger? logger = null) + { + _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory)); + + if (string.IsNullOrWhiteSpace(cacheDirectory)) + throw new ArgumentException("Cache directory is required.", nameof(cacheDirectory)); + + if (!Directory.Exists(cacheDirectory)) + throw new DirectoryNotFoundException($"Cache directory not found: {cacheDirectory}"); + + _cacheDirectory = cacheDirectory; + _logger = logger; + } + + public IEnumerable GetAvailableTables() => _pipelineFactories.Keys; + + public EtlPipeline GetPipeline(string tableName) + { + if (!_pipelineFactories.TryGetValue(tableName, out var factory)) + throw new ArgumentException($"No pipeline registered for table '{tableName}'.", nameof(tableName)); + + return factory(_connectionFactory, _cacheDirectory); + } + + public async Task RunAsync(string tableName, CancellationToken cancellationToken = default) + { + _logger?.LogInformation("Running dev ETL for {TableName}", tableName); + + var pipeline = GetPipeline(tableName); + var result = await pipeline.ExecuteAsync(cancellationToken); + + if (result.Success) + _logger?.LogInformation("Completed {TableName}: {Rows} rows in {Elapsed:g}", + tableName, result.TotalRows, result.Elapsed); + else + _logger?.LogError(result.Error, "Failed {TableName}: {Error}", + tableName, result.Error?.Message); + + return result; + } + + public async Task> RunAllAsync(CancellationToken cancellationToken = default) + { + var results = new List(); + + foreach (var tableName in GetAvailableTables()) + { + cancellationToken.ThrowIfCancellationRequested(); + var result = await RunAsync(tableName, cancellationToken); + results.Add(result); + } + + return results; + } + + /// + /// Runs all dev ETL pipelines with parallelization. + /// Small/medium tables run concurrently, very large tables run sequentially at the end. + /// + /// Maximum concurrent table loads (default 4). + /// Cancellation token. + public async Task> RunAllParallelAsync( + int maxDegreeOfParallelism = 4, + CancellationToken cancellationToken = default) + { + var results = new ConcurrentBag(); + using var semaphore = new SemaphoreSlim(maxDegreeOfParallelism); + + // Separate tables by size - run very large ones sequentially at the end + var smallMediumTables = GetAvailableTables() + .Where(t => !IsVeryLargeTable(t)) + .ToList(); + var veryLargeTables = GetAvailableTables() + .Where(IsVeryLargeTable) + .ToList(); + + _logger?.LogInformation( + "Running {ParallelCount} tables in parallel (max {MaxParallel}), then {SequentialCount} large tables sequentially", + smallMediumTables.Count, maxDegreeOfParallelism, veryLargeTables.Count); + + // Run small/medium tables in parallel + var tasks = smallMediumTables.Select(async tableName => + { + await semaphore.WaitAsync(cancellationToken); + try + { + var result = await RunAsync(tableName, cancellationToken); + results.Add(result); + } + finally + { + semaphore.Release(); + } + }); + + await Task.WhenAll(tasks); + + // Run very large tables sequentially (IO-bound, would contend) + foreach (var tableName in veryLargeTables) + { + cancellationToken.ThrowIfCancellationRequested(); + var result = await RunAsync(tableName, cancellationToken); + results.Add(result); + } + + return results.ToList(); + } + + /// + /// Identifies very large tables that should be loaded sequentially to avoid IO contention. + /// + private static bool IsVeryLargeTable(string tableName) => + tableName.Contains("WorkOrderTime", StringComparison.OrdinalIgnoreCase) || + tableName.Contains("WorkOrderStep", StringComparison.OrdinalIgnoreCase) || + tableName.Contains("WorkOrderRouting", StringComparison.OrdinalIgnoreCase) || + tableName.Contains("WorkOrderComponent", StringComparison.OrdinalIgnoreCase) || + tableName.Contains("LotUsage", StringComparison.OrdinalIgnoreCase); +} diff --git a/NEW/src/JdeScoping.DataSync.Dev/FunctionCodeDevEtl.cs b/NEW/src/JdeScoping.DataSync.Dev/FunctionCodeDevEtl.cs new file mode 100644 index 0000000..11d13c7 --- /dev/null +++ b/NEW/src/JdeScoping.DataSync.Dev/FunctionCodeDevEtl.cs @@ -0,0 +1,38 @@ +using JdeScoping.DataAccess.Interfaces; +using JdeScoping.DataSync.Etl.Destinations; +using JdeScoping.DataSync.Etl.Models; +using JdeScoping.DataSync.Etl.Pipeline; +using JdeScoping.DataSync.Etl.Sources; + +namespace JdeScoping.DataSync.Dev; + +/// +/// Development ETL pipeline for the FunctionCode table. +/// Schema from: Scripts/005_CreateFunctionCodeTable.sql +/// +public static class FunctionCodeDevEtl +{ + public static readonly string TableName = "FunctionCode"; + public static readonly string CacheFileName = "functioncode.json.zstd"; + + private static readonly JsonColumnSchema[] Schema = + [ + new("Code", typeof(string), IsNullable: false), + new("Description", typeof(string), IsNullable: true), + new("LastUpdateDT", typeof(DateTime), IsNullable: false), + ]; + + public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath) + { + ArgumentNullException.ThrowIfNull(connectionFactory); + + if (string.IsNullOrWhiteSpace(cacheFilePath)) + throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath)); + + return new EtlPipelineBuilder() + .WithName($"{TableName}_Dev") + .WithSource(new JsonZstdFileSource(cacheFilePath, Schema)) + .WithDestination(new DbBulkImportDestination(connectionFactory, TableName)) + .Build(); + } +} diff --git a/NEW/src/JdeScoping.DataSync.Dev/ItemDevEtl.cs b/NEW/src/JdeScoping.DataSync.Dev/ItemDevEtl.cs new file mode 100644 index 0000000..8d58929 --- /dev/null +++ b/NEW/src/JdeScoping.DataSync.Dev/ItemDevEtl.cs @@ -0,0 +1,41 @@ +using JdeScoping.DataAccess.Interfaces; +using JdeScoping.DataSync.Etl.Destinations; +using JdeScoping.DataSync.Etl.Models; +using JdeScoping.DataSync.Etl.Pipeline; +using JdeScoping.DataSync.Etl.Sources; + +namespace JdeScoping.DataSync.Dev; + +/// +/// Development ETL pipeline for the Item table. +/// Schema from: Scripts/008_CreateItemTable.sql +/// +public static class ItemDevEtl +{ + public static readonly string TableName = "Item"; + public static readonly string CacheFileName = "item.json.zstd"; + + private static readonly JsonColumnSchema[] Schema = + [ + new("ShortItemNumber", typeof(long), IsNullable: false), + new("ItemNumber", typeof(string), IsNullable: false), + new("Description", typeof(string), IsNullable: true), + new("PlanningFamily", typeof(string), IsNullable: true), + new("StockingType", typeof(string), IsNullable: true), + new("LastUpdateDT", typeof(DateTime), IsNullable: false), + ]; + + public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath) + { + ArgumentNullException.ThrowIfNull(connectionFactory); + + if (string.IsNullOrWhiteSpace(cacheFilePath)) + throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath)); + + return new EtlPipelineBuilder() + .WithName($"{TableName}_Dev") + .WithSource(new JsonZstdFileSource(cacheFilePath, Schema)) + .WithDestination(new DbBulkImportDestination(connectionFactory, TableName)) + .Build(); + } +} diff --git a/NEW/src/JdeScoping.DataSync.Dev/JdeScoping.DataSync.Dev.csproj b/NEW/src/JdeScoping.DataSync.Dev/JdeScoping.DataSync.Dev.csproj new file mode 100644 index 0000000..223ab5c --- /dev/null +++ b/NEW/src/JdeScoping.DataSync.Dev/JdeScoping.DataSync.Dev.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/NEW/src/JdeScoping.DataSync.Dev/JdeUserDevEtl.cs b/NEW/src/JdeScoping.DataSync.Dev/JdeUserDevEtl.cs new file mode 100644 index 0000000..419247c --- /dev/null +++ b/NEW/src/JdeScoping.DataSync.Dev/JdeUserDevEtl.cs @@ -0,0 +1,39 @@ +using JdeScoping.DataAccess.Interfaces; +using JdeScoping.DataSync.Etl.Destinations; +using JdeScoping.DataSync.Etl.Models; +using JdeScoping.DataSync.Etl.Pipeline; +using JdeScoping.DataSync.Etl.Sources; + +namespace JdeScoping.DataSync.Dev; + +/// +/// Development ETL pipeline for the JdeUser table. +/// Schema from: Scripts/009_CreateJdeUserTable.sql +/// +public static class JdeUserDevEtl +{ + public static readonly string TableName = "JdeUser"; + public static readonly string CacheFileName = "jdeuser.json.zstd"; + + private static readonly JsonColumnSchema[] Schema = + [ + new("AddressNumber", typeof(long), IsNullable: false), + new("UserID", typeof(string), IsNullable: true), + new("FullName", typeof(string), IsNullable: false), + new("LastUpdateDT", typeof(DateTime), IsNullable: false), + ]; + + public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath) + { + ArgumentNullException.ThrowIfNull(connectionFactory); + + if (string.IsNullOrWhiteSpace(cacheFilePath)) + throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath)); + + return new EtlPipelineBuilder() + .WithName($"{TableName}_Dev") + .WithSource(new JsonZstdFileSource(cacheFilePath, Schema)) + .WithDestination(new DbBulkImportDestination(connectionFactory, TableName)) + .Build(); + } +} diff --git a/NEW/src/JdeScoping.DataSync.Dev/LotDevEtl.cs b/NEW/src/JdeScoping.DataSync.Dev/LotDevEtl.cs new file mode 100644 index 0000000..643c4e4 --- /dev/null +++ b/NEW/src/JdeScoping.DataSync.Dev/LotDevEtl.cs @@ -0,0 +1,45 @@ +using JdeScoping.DataAccess.Interfaces; +using JdeScoping.DataSync.Etl.Destinations; +using JdeScoping.DataSync.Etl.Models; +using JdeScoping.DataSync.Etl.Pipeline; +using JdeScoping.DataSync.Etl.Sources; + +namespace JdeScoping.DataSync.Dev; + +/// +/// Development ETL pipeline for the Lot table. +/// Schema from: Scripts/013_CreateLotTable.sql +/// +public static class LotDevEtl +{ + public static readonly string TableName = "Lot"; + public static readonly string CacheFileName = "lot.json.zstd"; + + private static readonly JsonColumnSchema[] Schema = + [ + new("LotNumber", typeof(string), IsNullable: false), + new("BranchCode", typeof(string), IsNullable: false), + new("ShortItemNumber", typeof(long), IsNullable: false), + new("ItemNumber", typeof(string), IsNullable: true), + new("SupplierCode", typeof(long), IsNullable: false), + new("StatusCode", typeof(string), IsNullable: true), + new("Memo1", typeof(string), IsNullable: true), + new("Memo2", typeof(string), IsNullable: true), + new("Memo3", typeof(string), IsNullable: true), + new("LastUpdateDT", typeof(DateTime), IsNullable: false), + ]; + + public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath) + { + ArgumentNullException.ThrowIfNull(connectionFactory); + + if (string.IsNullOrWhiteSpace(cacheFilePath)) + throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath)); + + return new EtlPipelineBuilder() + .WithName($"{TableName}_Dev") + .WithSource(new JsonZstdFileSource(cacheFilePath, Schema)) + .WithDestination(new DbBulkImportDestination(connectionFactory, TableName)) + .Build(); + } +} diff --git a/NEW/src/JdeScoping.DataSync.Dev/LotUsageCurrDevEtl.cs b/NEW/src/JdeScoping.DataSync.Dev/LotUsageCurrDevEtl.cs new file mode 100644 index 0000000..7d580e9 --- /dev/null +++ b/NEW/src/JdeScoping.DataSync.Dev/LotUsageCurrDevEtl.cs @@ -0,0 +1,42 @@ +using JdeScoping.DataAccess.Interfaces; +using JdeScoping.DataSync.Etl.Destinations; +using JdeScoping.DataSync.Etl.Models; +using JdeScoping.DataSync.Etl.Pipeline; +using JdeScoping.DataSync.Etl.Sources; + +namespace JdeScoping.DataSync.Dev; + +/// +/// Development ETL pipeline for the LotUsage_Curr table. +/// Schema from: Scripts/024_CreateLotUsageCurrTable.sql +/// +public static class LotUsageCurrDevEtl +{ + public static readonly string TableName = "LotUsage_Curr"; + public static readonly string CacheFileName = "lotusage_curr.json.zstd"; + + private static readonly JsonColumnSchema[] Schema = + [ + new("UniqueID", typeof(long), IsNullable: false), + new("WorkOrderNumber", typeof(long), IsNullable: false), + new("LotNumber", typeof(string), IsNullable: false), + new("BranchCode", typeof(string), IsNullable: true), + new("ShortItemNumber", typeof(long), IsNullable: false), + new("Quantity", typeof(decimal), IsNullable: false), + new("LastUpdateDT", typeof(DateTime), IsNullable: false), + ]; + + public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath) + { + ArgumentNullException.ThrowIfNull(connectionFactory); + + if (string.IsNullOrWhiteSpace(cacheFilePath)) + throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath)); + + return new EtlPipelineBuilder() + .WithName($"{TableName}_Dev") + .WithSource(new JsonZstdFileSource(cacheFilePath, Schema)) + .WithDestination(new DbBulkImportDestination(connectionFactory, TableName)) + .Build(); + } +} diff --git a/NEW/src/JdeScoping.DataSync.Dev/LotUsageHistDevEtl.cs b/NEW/src/JdeScoping.DataSync.Dev/LotUsageHistDevEtl.cs new file mode 100644 index 0000000..7c9079e --- /dev/null +++ b/NEW/src/JdeScoping.DataSync.Dev/LotUsageHistDevEtl.cs @@ -0,0 +1,42 @@ +using JdeScoping.DataAccess.Interfaces; +using JdeScoping.DataSync.Etl.Destinations; +using JdeScoping.DataSync.Etl.Models; +using JdeScoping.DataSync.Etl.Pipeline; +using JdeScoping.DataSync.Etl.Sources; + +namespace JdeScoping.DataSync.Dev; + +/// +/// Development ETL pipeline for the LotUsage_Hist table. +/// Schema from: Scripts/025_CreateLotUsageHistTable.sql +/// +public static class LotUsageHistDevEtl +{ + public static readonly string TableName = "LotUsage_Hist"; + public static readonly string CacheFileName = "lotusage_hist.json.zstd"; + + private static readonly JsonColumnSchema[] Schema = + [ + new("UniqueID", typeof(long), IsNullable: false), + new("WorkOrderNumber", typeof(long), IsNullable: false), + new("LotNumber", typeof(string), IsNullable: false), + new("BranchCode", typeof(string), IsNullable: true), + new("ShortItemNumber", typeof(long), IsNullable: false), + new("Quantity", typeof(decimal), IsNullable: false), + new("LastUpdateDT", typeof(DateTime), IsNullable: false), + ]; + + public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath) + { + ArgumentNullException.ThrowIfNull(connectionFactory); + + if (string.IsNullOrWhiteSpace(cacheFilePath)) + throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath)); + + return new EtlPipelineBuilder() + .WithName($"{TableName}_Dev") + .WithSource(new JsonZstdFileSource(cacheFilePath, Schema)) + .WithDestination(new DbBulkImportDestination(connectionFactory, TableName)) + .Build(); + } +} diff --git a/NEW/src/JdeScoping.DataSync.Dev/MisDataDevEtl.cs b/NEW/src/JdeScoping.DataSync.Dev/MisDataDevEtl.cs new file mode 100644 index 0000000..4d07326 --- /dev/null +++ b/NEW/src/JdeScoping.DataSync.Dev/MisDataDevEtl.cs @@ -0,0 +1,49 @@ +using JdeScoping.DataAccess.Interfaces; +using JdeScoping.DataSync.Etl.Destinations; +using JdeScoping.DataSync.Etl.Models; +using JdeScoping.DataSync.Etl.Pipeline; +using JdeScoping.DataSync.Etl.Sources; + +namespace JdeScoping.DataSync.Dev; + +/// +/// Development ETL pipeline for the MisData table. +/// Schema from: Scripts/012_CreateMisDataTable.sql +/// +public static class MisDataDevEtl +{ + public static readonly string TableName = "MisData"; + public static readonly string CacheFileName = "misdata.json.zstd"; + + private static readonly JsonColumnSchema[] Schema = + [ + new("ItemNumber", typeof(string), IsNullable: false), + new("BranchCode", typeof(string), IsNullable: false), + new("SequenceNumber", typeof(string), IsNullable: false), + new("MisNumber", typeof(string), IsNullable: false), + new("RevID", typeof(string), IsNullable: false), + new("CharNumber", typeof(string), IsNullable: false), + new("TestDescription", typeof(string), IsNullable: true), + new("SamplingType", typeof(string), IsNullable: true), + new("SamplingValue", typeof(string), IsNullable: true), + new("ToolsGauges", typeof(string), IsNullable: true), + new("WorkInstructions", typeof(string), IsNullable: true), + new("Status", typeof(string), IsNullable: false), + new("ReleaseDate", typeof(DateTime), IsNullable: true), + new("ObsoleteDate", typeof(DateTime), IsNullable: true), + ]; + + public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath) + { + ArgumentNullException.ThrowIfNull(connectionFactory); + + if (string.IsNullOrWhiteSpace(cacheFilePath)) + throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath)); + + return new EtlPipelineBuilder() + .WithName($"{TableName}_Dev") + .WithSource(new JsonZstdFileSource(cacheFilePath, Schema)) + .WithDestination(new DbBulkImportDestination(connectionFactory, TableName)) + .Build(); + } +} diff --git a/NEW/src/JdeScoping.DataSync.Dev/OrgHierarchyDevEtl.cs b/NEW/src/JdeScoping.DataSync.Dev/OrgHierarchyDevEtl.cs new file mode 100644 index 0000000..f198da0 --- /dev/null +++ b/NEW/src/JdeScoping.DataSync.Dev/OrgHierarchyDevEtl.cs @@ -0,0 +1,39 @@ +using JdeScoping.DataAccess.Interfaces; +using JdeScoping.DataSync.Etl.Destinations; +using JdeScoping.DataSync.Etl.Models; +using JdeScoping.DataSync.Etl.Pipeline; +using JdeScoping.DataSync.Etl.Sources; + +namespace JdeScoping.DataSync.Dev; + +/// +/// Development ETL pipeline for the OrgHierarchy table. +/// Schema from: Scripts/010_CreateOrgHierarchyTable.sql +/// +public static class OrgHierarchyDevEtl +{ + public static readonly string TableName = "OrgHierarchy"; + public static readonly string CacheFileName = "orghierarchy.json.zstd"; + + private static readonly JsonColumnSchema[] Schema = + [ + new("WorkCenterCode", typeof(string), IsNullable: false), + new("BranchCode", typeof(string), IsNullable: false), + new("ProfitCenterCode", typeof(string), IsNullable: false), + new("LastUpdateDT", typeof(DateTime), IsNullable: false), + ]; + + public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath) + { + ArgumentNullException.ThrowIfNull(connectionFactory); + + if (string.IsNullOrWhiteSpace(cacheFilePath)) + throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath)); + + return new EtlPipelineBuilder() + .WithName($"{TableName}_Dev") + .WithSource(new JsonZstdFileSource(cacheFilePath, Schema)) + .WithDestination(new DbBulkImportDestination(connectionFactory, TableName)) + .Build(); + } +} diff --git a/NEW/src/JdeScoping.DataSync.Dev/ProfitCenterDevEtl.cs b/NEW/src/JdeScoping.DataSync.Dev/ProfitCenterDevEtl.cs new file mode 100644 index 0000000..7dbb96a --- /dev/null +++ b/NEW/src/JdeScoping.DataSync.Dev/ProfitCenterDevEtl.cs @@ -0,0 +1,38 @@ +using JdeScoping.DataAccess.Interfaces; +using JdeScoping.DataSync.Etl.Destinations; +using JdeScoping.DataSync.Etl.Models; +using JdeScoping.DataSync.Etl.Pipeline; +using JdeScoping.DataSync.Etl.Sources; + +namespace JdeScoping.DataSync.Dev; + +/// +/// Development ETL pipeline for the ProfitCenter table. +/// Schema from: Scripts/006_CreateProfitCenterTable.sql +/// +public static class ProfitCenterDevEtl +{ + public static readonly string TableName = "ProfitCenter"; + public static readonly string CacheFileName = "profitcenter.json.zstd"; + + private static readonly JsonColumnSchema[] Schema = + [ + new("Code", typeof(string), IsNullable: false), + new("Description", typeof(string), IsNullable: true), + new("LastUpdateDT", typeof(DateTime), IsNullable: false), + ]; + + public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath) + { + ArgumentNullException.ThrowIfNull(connectionFactory); + + if (string.IsNullOrWhiteSpace(cacheFilePath)) + throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath)); + + return new EtlPipelineBuilder() + .WithName($"{TableName}_Dev") + .WithSource(new JsonZstdFileSource(cacheFilePath, Schema)) + .WithDestination(new DbBulkImportDestination(connectionFactory, TableName)) + .Build(); + } +} diff --git a/NEW/src/JdeScoping.DataSync.Dev/RouteMasterDevEtl.cs b/NEW/src/JdeScoping.DataSync.Dev/RouteMasterDevEtl.cs new file mode 100644 index 0000000..04f8116 --- /dev/null +++ b/NEW/src/JdeScoping.DataSync.Dev/RouteMasterDevEtl.cs @@ -0,0 +1,44 @@ +using JdeScoping.DataAccess.Interfaces; +using JdeScoping.DataSync.Etl.Destinations; +using JdeScoping.DataSync.Etl.Models; +using JdeScoping.DataSync.Etl.Pipeline; +using JdeScoping.DataSync.Etl.Sources; + +namespace JdeScoping.DataSync.Dev; + +/// +/// Development ETL pipeline for the RouteMaster table. +/// Schema from: Scripts/011_CreateRouteMasterTable.sql +/// +public static class RouteMasterDevEtl +{ + public static readonly string TableName = "RouteMaster"; + public static readonly string CacheFileName = "routemaster.json.zstd"; + + private static readonly JsonColumnSchema[] Schema = + [ + new("BranchCode", typeof(string), IsNullable: false), + new("ItemNumber", typeof(string), IsNullable: false), + new("RoutingType", typeof(string), IsNullable: false), + new("SequenceNumber", typeof(decimal), IsNullable: false), + new("FunctionCode", typeof(string), IsNullable: true), + new("WorkCenterCode", typeof(string), IsNullable: true), + new("StartDate", typeof(DateTime), IsNullable: false), + new("EndDate", typeof(DateTime), IsNullable: true), + new("LastUpdateDT", typeof(DateTime), IsNullable: false), + ]; + + public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath) + { + ArgumentNullException.ThrowIfNull(connectionFactory); + + if (string.IsNullOrWhiteSpace(cacheFilePath)) + throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath)); + + return new EtlPipelineBuilder() + .WithName($"{TableName}_Dev") + .WithSource(new JsonZstdFileSource(cacheFilePath, Schema)) + .WithDestination(new DbBulkImportDestination(connectionFactory, TableName)) + .Build(); + } +} diff --git a/NEW/src/JdeScoping.DataSync.Dev/WorkCenterDevEtl.cs b/NEW/src/JdeScoping.DataSync.Dev/WorkCenterDevEtl.cs new file mode 100644 index 0000000..21a121b --- /dev/null +++ b/NEW/src/JdeScoping.DataSync.Dev/WorkCenterDevEtl.cs @@ -0,0 +1,38 @@ +using JdeScoping.DataAccess.Interfaces; +using JdeScoping.DataSync.Etl.Destinations; +using JdeScoping.DataSync.Etl.Models; +using JdeScoping.DataSync.Etl.Pipeline; +using JdeScoping.DataSync.Etl.Sources; + +namespace JdeScoping.DataSync.Dev; + +/// +/// Development ETL pipeline for the WorkCenter table. +/// Schema from: Scripts/007_CreateWorkCenterTable.sql +/// +public static class WorkCenterDevEtl +{ + public static readonly string TableName = "WorkCenter"; + public static readonly string CacheFileName = "workcenter.json.zstd"; + + private static readonly JsonColumnSchema[] Schema = + [ + new("Code", typeof(string), IsNullable: false), + new("Description", typeof(string), IsNullable: true), + new("LastUpdateDT", typeof(DateTime), IsNullable: false), + ]; + + public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath) + { + ArgumentNullException.ThrowIfNull(connectionFactory); + + if (string.IsNullOrWhiteSpace(cacheFilePath)) + throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath)); + + return new EtlPipelineBuilder() + .WithName($"{TableName}_Dev") + .WithSource(new JsonZstdFileSource(cacheFilePath, Schema)) + .WithDestination(new DbBulkImportDestination(connectionFactory, TableName)) + .Build(); + } +} diff --git a/NEW/src/JdeScoping.DataSync.Dev/WorkOrderComponentCurrDevEtl.cs b/NEW/src/JdeScoping.DataSync.Dev/WorkOrderComponentCurrDevEtl.cs new file mode 100644 index 0000000..04a6409 --- /dev/null +++ b/NEW/src/JdeScoping.DataSync.Dev/WorkOrderComponentCurrDevEtl.cs @@ -0,0 +1,42 @@ +using JdeScoping.DataAccess.Interfaces; +using JdeScoping.DataSync.Etl.Destinations; +using JdeScoping.DataSync.Etl.Models; +using JdeScoping.DataSync.Etl.Pipeline; +using JdeScoping.DataSync.Etl.Sources; + +namespace JdeScoping.DataSync.Dev; + +/// +/// Development ETL pipeline for the WorkOrderComponent_Curr table. +/// Schema from: Scripts/021_CreateWorkOrderComponentCurrTable.sql +/// +public static class WorkOrderComponentCurrDevEtl +{ + public static readonly string TableName = "WorkOrderComponent_Curr"; + public static readonly string CacheFileName = "workordercomponent_curr.json.zstd"; + + private static readonly JsonColumnSchema[] Schema = + [ + new("UniqueID", typeof(long), IsNullable: false), + new("WorkOrderNumber", typeof(long), IsNullable: false), + new("LotNumber", typeof(string), IsNullable: false), + new("BranchCode", typeof(string), IsNullable: true), + new("ShortItemNumber", typeof(long), IsNullable: false), + new("Quantity", typeof(decimal), IsNullable: false), + new("LastUpdateDT", typeof(DateTime), IsNullable: false), + ]; + + public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath) + { + ArgumentNullException.ThrowIfNull(connectionFactory); + + if (string.IsNullOrWhiteSpace(cacheFilePath)) + throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath)); + + return new EtlPipelineBuilder() + .WithName($"{TableName}_Dev") + .WithSource(new JsonZstdFileSource(cacheFilePath, Schema)) + .WithDestination(new DbBulkImportDestination(connectionFactory, TableName)) + .Build(); + } +} diff --git a/NEW/src/JdeScoping.DataSync.Dev/WorkOrderComponentHistDevEtl.cs b/NEW/src/JdeScoping.DataSync.Dev/WorkOrderComponentHistDevEtl.cs new file mode 100644 index 0000000..e19ad48 --- /dev/null +++ b/NEW/src/JdeScoping.DataSync.Dev/WorkOrderComponentHistDevEtl.cs @@ -0,0 +1,42 @@ +using JdeScoping.DataAccess.Interfaces; +using JdeScoping.DataSync.Etl.Destinations; +using JdeScoping.DataSync.Etl.Models; +using JdeScoping.DataSync.Etl.Pipeline; +using JdeScoping.DataSync.Etl.Sources; + +namespace JdeScoping.DataSync.Dev; + +/// +/// Development ETL pipeline for the WorkOrderComponent_Hist table. +/// Schema from: Scripts/022_CreateWorkOrderComponentHistTable.sql +/// +public static class WorkOrderComponentHistDevEtl +{ + public static readonly string TableName = "WorkOrderComponent_Hist"; + public static readonly string CacheFileName = "workordercomponent_hist.json.zstd"; + + private static readonly JsonColumnSchema[] Schema = + [ + new("UniqueID", typeof(long), IsNullable: false), + new("WorkOrderNumber", typeof(long), IsNullable: false), + new("LotNumber", typeof(string), IsNullable: false), + new("BranchCode", typeof(string), IsNullable: true), + new("ShortItemNumber", typeof(long), IsNullable: false), + new("Quantity", typeof(decimal), IsNullable: false), + new("LastUpdateDT", typeof(DateTime), IsNullable: false), + ]; + + public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath) + { + ArgumentNullException.ThrowIfNull(connectionFactory); + + if (string.IsNullOrWhiteSpace(cacheFilePath)) + throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath)); + + return new EtlPipelineBuilder() + .WithName($"{TableName}_Dev") + .WithSource(new JsonZstdFileSource(cacheFilePath, Schema)) + .WithDestination(new DbBulkImportDestination(connectionFactory, TableName)) + .Build(); + } +} diff --git a/NEW/src/JdeScoping.DataSync.Dev/WorkOrderCurrDevEtl.cs b/NEW/src/JdeScoping.DataSync.Dev/WorkOrderCurrDevEtl.cs new file mode 100644 index 0000000..eab6217 --- /dev/null +++ b/NEW/src/JdeScoping.DataSync.Dev/WorkOrderCurrDevEtl.cs @@ -0,0 +1,50 @@ +using JdeScoping.DataAccess.Interfaces; +using JdeScoping.DataSync.Etl.Destinations; +using JdeScoping.DataSync.Etl.Models; +using JdeScoping.DataSync.Etl.Pipeline; +using JdeScoping.DataSync.Etl.Sources; + +namespace JdeScoping.DataSync.Dev; + +/// +/// Development ETL pipeline for the WorkOrder_Curr table. +/// Schema from: Scripts/015_CreateWorkOrderCurrTable.sql +/// +public static class WorkOrderCurrDevEtl +{ + public static readonly string TableName = "WorkOrder_Curr"; + public static readonly string CacheFileName = "workorder_curr.json.zstd"; + + private static readonly JsonColumnSchema[] Schema = + [ + new("WorkOrderNumber", typeof(long), IsNullable: false), + new("BranchCode", typeof(string), IsNullable: true), + new("LotNumber", typeof(string), IsNullable: true), + new("ItemNumber", typeof(string), IsNullable: true), + new("ShortItemNumber", typeof(long), IsNullable: false), + new("ParentWorkOrderNumber", typeof(string), IsNullable: true), + new("OrderQuantity", typeof(decimal), IsNullable: false), + new("HeldQuantity", typeof(decimal), IsNullable: false), + new("ShippedQuantity", typeof(decimal), IsNullable: false), + new("StatusCode", typeof(string), IsNullable: true), + new("StatusCodeUpdateDT", typeof(DateTime), IsNullable: true), + new("IssueDate", typeof(DateTime), IsNullable: false), + new("StartDate", typeof(DateTime), IsNullable: false), + new("RoutingType", typeof(string), IsNullable: true), + new("LastUpdateDT", typeof(DateTime), IsNullable: false), + ]; + + public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath) + { + ArgumentNullException.ThrowIfNull(connectionFactory); + + if (string.IsNullOrWhiteSpace(cacheFilePath)) + throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath)); + + return new EtlPipelineBuilder() + .WithName($"{TableName}_Dev") + .WithSource(new JsonZstdFileSource(cacheFilePath, Schema)) + .WithDestination(new DbBulkImportDestination(connectionFactory, TableName)) + .Build(); + } +} diff --git a/NEW/src/JdeScoping.DataSync.Dev/WorkOrderHistDevEtl.cs b/NEW/src/JdeScoping.DataSync.Dev/WorkOrderHistDevEtl.cs new file mode 100644 index 0000000..0c8b3fc --- /dev/null +++ b/NEW/src/JdeScoping.DataSync.Dev/WorkOrderHistDevEtl.cs @@ -0,0 +1,50 @@ +using JdeScoping.DataAccess.Interfaces; +using JdeScoping.DataSync.Etl.Destinations; +using JdeScoping.DataSync.Etl.Models; +using JdeScoping.DataSync.Etl.Pipeline; +using JdeScoping.DataSync.Etl.Sources; + +namespace JdeScoping.DataSync.Dev; + +/// +/// Development ETL pipeline for the WorkOrder_Hist table. +/// Schema from: Scripts/016_CreateWorkOrderHistTable.sql +/// +public static class WorkOrderHistDevEtl +{ + public static readonly string TableName = "WorkOrder_Hist"; + public static readonly string CacheFileName = "workorder_hist.json.zstd"; + + private static readonly JsonColumnSchema[] Schema = + [ + new("WorkOrderNumber", typeof(long), IsNullable: false), + new("BranchCode", typeof(string), IsNullable: true), + new("LotNumber", typeof(string), IsNullable: true), + new("ItemNumber", typeof(string), IsNullable: true), + new("ShortItemNumber", typeof(long), IsNullable: false), + new("ParentWorkOrderNumber", typeof(string), IsNullable: true), + new("OrderQuantity", typeof(decimal), IsNullable: false), + new("HeldQuantity", typeof(decimal), IsNullable: false), + new("ShippedQuantity", typeof(decimal), IsNullable: false), + new("StatusCode", typeof(string), IsNullable: true), + new("StatusCodeUpdateDT", typeof(DateTime), IsNullable: true), + new("IssueDate", typeof(DateTime), IsNullable: false), + new("StartDate", typeof(DateTime), IsNullable: false), + new("RoutingType", typeof(string), IsNullable: true), + new("LastUpdateDT", typeof(DateTime), IsNullable: false), + ]; + + public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath) + { + ArgumentNullException.ThrowIfNull(connectionFactory); + + if (string.IsNullOrWhiteSpace(cacheFilePath)) + throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath)); + + return new EtlPipelineBuilder() + .WithName($"{TableName}_Dev") + .WithSource(new JsonZstdFileSource(cacheFilePath, Schema)) + .WithDestination(new DbBulkImportDestination(connectionFactory, TableName)) + .Build(); + } +} diff --git a/NEW/src/JdeScoping.DataSync.Dev/WorkOrderRoutingDevEtl.cs b/NEW/src/JdeScoping.DataSync.Dev/WorkOrderRoutingDevEtl.cs new file mode 100644 index 0000000..1b01255 --- /dev/null +++ b/NEW/src/JdeScoping.DataSync.Dev/WorkOrderRoutingDevEtl.cs @@ -0,0 +1,48 @@ +using JdeScoping.DataAccess.Interfaces; +using JdeScoping.DataSync.Etl.Destinations; +using JdeScoping.DataSync.Etl.Models; +using JdeScoping.DataSync.Etl.Pipeline; +using JdeScoping.DataSync.Etl.Sources; + +namespace JdeScoping.DataSync.Dev; + +/// +/// Development ETL pipeline for the WorkOrderRouting table. +/// Schema from: Scripts/023_CreateWorkOrderRoutingTable.sql +/// +public static class WorkOrderRoutingDevEtl +{ + public static readonly string TableName = "WorkOrderRouting"; + public static readonly string CacheFileName = "workorderrouting.json.zstd"; + + private static readonly JsonColumnSchema[] Schema = + [ + new("UserID", typeof(string), IsNullable: false), + new("BatchNumber", typeof(string), IsNullable: false), + new("TransactionNumber", typeof(string), IsNullable: false), + new("LineNumber", typeof(int), IsNullable: false), + new("StepNumber", typeof(decimal), IsNullable: false), + new("WorkCenterCode", typeof(string), IsNullable: false), + new("WorkOrderNumber", typeof(long), IsNullable: false), + new("RoutingType", typeof(string), IsNullable: true), + new("BranchCode", typeof(string), IsNullable: true), + new("StepDescription", typeof(string), IsNullable: true), + new("FunctionCode", typeof(string), IsNullable: true), + new("TransactionDate", typeof(DateTime), IsNullable: false), + new("LastUpdateDT", typeof(DateTime), IsNullable: false), + ]; + + public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath) + { + ArgumentNullException.ThrowIfNull(connectionFactory); + + if (string.IsNullOrWhiteSpace(cacheFilePath)) + throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath)); + + return new EtlPipelineBuilder() + .WithName($"{TableName}_Dev") + .WithSource(new JsonZstdFileSource(cacheFilePath, Schema)) + .WithDestination(new DbBulkImportDestination(connectionFactory, TableName)) + .Build(); + } +} diff --git a/NEW/src/JdeScoping.DataSync.Dev/WorkOrderStepCurrDevEtl.cs b/NEW/src/JdeScoping.DataSync.Dev/WorkOrderStepCurrDevEtl.cs new file mode 100644 index 0000000..52c7015 --- /dev/null +++ b/NEW/src/JdeScoping.DataSync.Dev/WorkOrderStepCurrDevEtl.cs @@ -0,0 +1,46 @@ +using JdeScoping.DataAccess.Interfaces; +using JdeScoping.DataSync.Etl.Destinations; +using JdeScoping.DataSync.Etl.Models; +using JdeScoping.DataSync.Etl.Pipeline; +using JdeScoping.DataSync.Etl.Sources; + +namespace JdeScoping.DataSync.Dev; + +/// +/// Development ETL pipeline for the WorkOrderStep_Curr table. +/// Schema from: Scripts/017_CreateWorkOrderStepCurrTable.sql +/// +public static class WorkOrderStepCurrDevEtl +{ + public static readonly string TableName = "WorkOrderStep_Curr"; + public static readonly string CacheFileName = "workorderstep_curr.json.zstd"; + + private static readonly JsonColumnSchema[] Schema = + [ + new("WorkOrderNumber", typeof(long), IsNullable: false), + new("WorkCenterCode", typeof(string), IsNullable: false), + new("StepNumber", typeof(decimal), IsNullable: false), + new("StepTypeCode", typeof(string), IsNullable: false), + new("BranchCode", typeof(string), IsNullable: false), + new("StepDescription", typeof(string), IsNullable: true), + new("StartDT", typeof(DateTime), IsNullable: true), + new("EndDT", typeof(DateTime), IsNullable: true), + new("FunctionCode", typeof(string), IsNullable: true), + new("ScrappedQuantity", typeof(decimal), IsNullable: false), + new("LastUpdateDT", typeof(DateTime), IsNullable: false), + ]; + + public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath) + { + ArgumentNullException.ThrowIfNull(connectionFactory); + + if (string.IsNullOrWhiteSpace(cacheFilePath)) + throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath)); + + return new EtlPipelineBuilder() + .WithName($"{TableName}_Dev") + .WithSource(new JsonZstdFileSource(cacheFilePath, Schema)) + .WithDestination(new DbBulkImportDestination(connectionFactory, TableName)) + .Build(); + } +} diff --git a/NEW/src/JdeScoping.DataSync.Dev/WorkOrderStepHistDevEtl.cs b/NEW/src/JdeScoping.DataSync.Dev/WorkOrderStepHistDevEtl.cs new file mode 100644 index 0000000..63b4be5 --- /dev/null +++ b/NEW/src/JdeScoping.DataSync.Dev/WorkOrderStepHistDevEtl.cs @@ -0,0 +1,46 @@ +using JdeScoping.DataAccess.Interfaces; +using JdeScoping.DataSync.Etl.Destinations; +using JdeScoping.DataSync.Etl.Models; +using JdeScoping.DataSync.Etl.Pipeline; +using JdeScoping.DataSync.Etl.Sources; + +namespace JdeScoping.DataSync.Dev; + +/// +/// Development ETL pipeline for the WorkOrderStep_Hist table. +/// Schema from: Scripts/018_CreateWorkOrderStepHistTable.sql +/// +public static class WorkOrderStepHistDevEtl +{ + public static readonly string TableName = "WorkOrderStep_Hist"; + public static readonly string CacheFileName = "workorderstep_hist.json.zstd"; + + private static readonly JsonColumnSchema[] Schema = + [ + new("WorkOrderNumber", typeof(long), IsNullable: false), + new("WorkCenterCode", typeof(string), IsNullable: false), + new("StepNumber", typeof(decimal), IsNullable: false), + new("StepTypeCode", typeof(string), IsNullable: false), + new("BranchCode", typeof(string), IsNullable: false), + new("StepDescription", typeof(string), IsNullable: true), + new("StartDT", typeof(DateTime), IsNullable: true), + new("EndDT", typeof(DateTime), IsNullable: true), + new("FunctionCode", typeof(string), IsNullable: true), + new("ScrappedQuantity", typeof(decimal), IsNullable: false), + new("LastUpdateDT", typeof(DateTime), IsNullable: false), + ]; + + public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath) + { + ArgumentNullException.ThrowIfNull(connectionFactory); + + if (string.IsNullOrWhiteSpace(cacheFilePath)) + throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath)); + + return new EtlPipelineBuilder() + .WithName($"{TableName}_Dev") + .WithSource(new JsonZstdFileSource(cacheFilePath, Schema)) + .WithDestination(new DbBulkImportDestination(connectionFactory, TableName)) + .Build(); + } +} diff --git a/NEW/src/JdeScoping.DataSync.Dev/WorkOrderTimeCurrDevEtl.cs b/NEW/src/JdeScoping.DataSync.Dev/WorkOrderTimeCurrDevEtl.cs new file mode 100644 index 0000000..14ff1f2 --- /dev/null +++ b/NEW/src/JdeScoping.DataSync.Dev/WorkOrderTimeCurrDevEtl.cs @@ -0,0 +1,43 @@ +using JdeScoping.DataAccess.Interfaces; +using JdeScoping.DataSync.Etl.Destinations; +using JdeScoping.DataSync.Etl.Models; +using JdeScoping.DataSync.Etl.Pipeline; +using JdeScoping.DataSync.Etl.Sources; + +namespace JdeScoping.DataSync.Dev; + +/// +/// Development ETL pipeline for the WorkOrderTime_Curr table. +/// Schema from: Scripts/019_CreateWorkOrderTimeCurrTable.sql +/// +public static class WorkOrderTimeCurrDevEtl +{ + public static readonly string TableName = "WorkOrderTime_Curr"; + public static readonly string CacheFileName = "workordertime_curr.json.zstd"; + + private static readonly JsonColumnSchema[] Schema = + [ + new("UniqueID", typeof(long), IsNullable: false), + new("WorkOrderNumber", typeof(long), IsNullable: false), + new("StepNumber", typeof(decimal), IsNullable: false), + new("WorkCenterCode", typeof(string), IsNullable: false), + new("BranchCode", typeof(string), IsNullable: false), + new("AddressNumber", typeof(long), IsNullable: false), + new("GlDate", typeof(DateTime), IsNullable: true), + new("LastUpdateDT", typeof(DateTime), IsNullable: false), + ]; + + public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath) + { + ArgumentNullException.ThrowIfNull(connectionFactory); + + if (string.IsNullOrWhiteSpace(cacheFilePath)) + throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath)); + + return new EtlPipelineBuilder() + .WithName($"{TableName}_Dev") + .WithSource(new JsonZstdFileSource(cacheFilePath, Schema)) + .WithDestination(new DbBulkImportDestination(connectionFactory, TableName)) + .Build(); + } +} diff --git a/NEW/src/JdeScoping.DataSync.Dev/WorkOrderTimeHistDevEtl.cs b/NEW/src/JdeScoping.DataSync.Dev/WorkOrderTimeHistDevEtl.cs new file mode 100644 index 0000000..ef63f89 --- /dev/null +++ b/NEW/src/JdeScoping.DataSync.Dev/WorkOrderTimeHistDevEtl.cs @@ -0,0 +1,43 @@ +using JdeScoping.DataAccess.Interfaces; +using JdeScoping.DataSync.Etl.Destinations; +using JdeScoping.DataSync.Etl.Models; +using JdeScoping.DataSync.Etl.Pipeline; +using JdeScoping.DataSync.Etl.Sources; + +namespace JdeScoping.DataSync.Dev; + +/// +/// Development ETL pipeline for the WorkOrderTime_Hist table. +/// Schema from: Scripts/020_CreateWorkOrderTimeHistTable.sql +/// +public static class WorkOrderTimeHistDevEtl +{ + public static readonly string TableName = "WorkOrderTime_Hist"; + public static readonly string CacheFileName = "workordertime_hist.json.zstd"; + + private static readonly JsonColumnSchema[] Schema = + [ + new("UniqueID", typeof(long), IsNullable: false), + new("WorkOrderNumber", typeof(long), IsNullable: false), + new("StepNumber", typeof(decimal), IsNullable: false), + new("WorkCenterCode", typeof(string), IsNullable: false), + new("BranchCode", typeof(string), IsNullable: false), + new("AddressNumber", typeof(long), IsNullable: false), + new("GlDate", typeof(DateTime), IsNullable: true), + new("LastUpdateDT", typeof(DateTime), IsNullable: false), + ]; + + public static EtlPipeline Create(IDbConnectionFactory connectionFactory, string cacheFilePath) + { + ArgumentNullException.ThrowIfNull(connectionFactory); + + if (string.IsNullOrWhiteSpace(cacheFilePath)) + throw new ArgumentException("Cache file path is required.", nameof(cacheFilePath)); + + return new EtlPipelineBuilder() + .WithName($"{TableName}_Dev") + .WithSource(new JsonZstdFileSource(cacheFilePath, Schema)) + .WithDestination(new DbBulkImportDestination(connectionFactory, TableName)) + .Build(); + } +} diff --git a/NEW/src/JdeScoping.DataSync/DevEtl/DevEtlRegistry.cs b/NEW/src/JdeScoping.DataSync/DevEtl/DevEtlRegistry.cs deleted file mode 100644 index e681043..0000000 --- a/NEW/src/JdeScoping.DataSync/DevEtl/DevEtlRegistry.cs +++ /dev/null @@ -1,80 +0,0 @@ -using JdeScoping.DataAccess.Interfaces; -using JdeScoping.DataSync.Etl.Pipeline; -using JdeScoping.DataSync.Etl.Results; -using Microsoft.Extensions.Logging; - -namespace JdeScoping.DataSync.DevEtl; - -/// -/// Registry for development ETL pipelines that load from cached JSON files. -/// -public class DevEtlRegistry -{ - private readonly IDbConnectionFactory _connectionFactory; - private readonly string _cacheDirectory; - private readonly ILogger? _logger; - - private readonly Dictionary> _pipelineFactories = new(StringComparer.OrdinalIgnoreCase) - { - [BranchDevEtl.TableName] = (factory, cacheDir) => - BranchDevEtl.Create(factory, Path.Combine(cacheDir, BranchDevEtl.CacheFileName)), - }; - - public DevEtlRegistry( - IDbConnectionFactory connectionFactory, - string cacheDirectory, - ILogger? logger = null) - { - _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory)); - - if (string.IsNullOrWhiteSpace(cacheDirectory)) - throw new ArgumentException("Cache directory is required.", nameof(cacheDirectory)); - - if (!Directory.Exists(cacheDirectory)) - throw new DirectoryNotFoundException($"Cache directory not found: {cacheDirectory}"); - - _cacheDirectory = cacheDirectory; - _logger = logger; - } - - public IEnumerable GetAvailableTables() => _pipelineFactories.Keys; - - public EtlPipeline GetPipeline(string tableName) - { - if (!_pipelineFactories.TryGetValue(tableName, out var factory)) - throw new ArgumentException($"No pipeline registered for table '{tableName}'.", nameof(tableName)); - - return factory(_connectionFactory, _cacheDirectory); - } - - public async Task RunAsync(string tableName, CancellationToken cancellationToken = default) - { - _logger?.LogInformation("Running dev ETL for {TableName}", tableName); - - var pipeline = GetPipeline(tableName); - var result = await pipeline.ExecuteAsync(cancellationToken); - - if (result.Success) - _logger?.LogInformation("Completed {TableName}: {Rows} rows in {Elapsed:g}", - tableName, result.TotalRows, result.Elapsed); - else - _logger?.LogError(result.Error, "Failed {TableName}: {Error}", - tableName, result.Error?.Message); - - return result; - } - - public async Task> RunAllAsync(CancellationToken cancellationToken = default) - { - var results = new List(); - - foreach (var tableName in GetAvailableTables()) - { - cancellationToken.ThrowIfCancellationRequested(); - var result = await RunAsync(tableName, cancellationToken); - results.Add(result); - } - - return results; - } -} diff --git a/NEW/tests/JdeScoping.DataSync.Tests/DevEtl/BranchDevEtlTests.cs b/NEW/tests/JdeScoping.DataSync.Dev.Tests/BranchDevEtlTests.cs similarity index 95% rename from NEW/tests/JdeScoping.DataSync.Tests/DevEtl/BranchDevEtlTests.cs rename to NEW/tests/JdeScoping.DataSync.Dev.Tests/BranchDevEtlTests.cs index 652df92..991c9ad 100644 --- a/NEW/tests/JdeScoping.DataSync.Tests/DevEtl/BranchDevEtlTests.cs +++ b/NEW/tests/JdeScoping.DataSync.Dev.Tests/BranchDevEtlTests.cs @@ -1,14 +1,14 @@ using Dapper; using JdeScoping.DataAccess; using JdeScoping.DataAccess.Interfaces; -using JdeScoping.DataSync.DevEtl; +using JdeScoping.DataSync.Dev; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; using Shouldly; -namespace JdeScoping.DataSync.Tests.DevEtl; +namespace JdeScoping.DataSync.Dev.Tests; /// /// Integration tests for Branch development ETL. diff --git a/NEW/tests/JdeScoping.DataSync.Dev.Tests/FunctionCodeDevEtlTests.cs b/NEW/tests/JdeScoping.DataSync.Dev.Tests/FunctionCodeDevEtlTests.cs new file mode 100644 index 0000000..44af0f9 --- /dev/null +++ b/NEW/tests/JdeScoping.DataSync.Dev.Tests/FunctionCodeDevEtlTests.cs @@ -0,0 +1,96 @@ +using Dapper; +using JdeScoping.DataAccess; +using JdeScoping.DataAccess.Interfaces; +using JdeScoping.DataSync.Dev; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Abstractions; +using NSubstitute; +using Shouldly; + +namespace JdeScoping.DataSync.Dev.Tests; + +/// +/// Integration tests for FunctionCode development ETL. +/// +public class FunctionCodeDevEtlTests : IAsyncLifetime +{ + private readonly string _connectionString; + private readonly string _cacheDirectory; + private readonly IDbConnectionFactory _connectionFactory; + + public FunctionCodeDevEtlTests() + { + var config = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true) + .AddEnvironmentVariables() + .Build(); + + _connectionString = config.GetConnectionString("LotFinderDB") + ?? throw new InvalidOperationException("LotFinderDB connection string not configured."); + + _cacheDirectory = config["DevEtl:CacheDirectory"] + ?? Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "CACHED_DB_FILES"); + + _connectionFactory = new DbConnectionFactory(config, NullLogger.Instance); + } + + public async Task InitializeAsync() + { + await using var connection = new SqlConnection(_connectionString); + await connection.OpenAsync(); + await connection.ExecuteAsync("TRUNCATE TABLE dbo.FunctionCode"); + } + + public Task DisposeAsync() => Task.CompletedTask; + + [Fact] + public void Create_ReturnsValidPipeline() + { + var cacheFilePath = Path.Combine(_cacheDirectory, FunctionCodeDevEtl.CacheFileName); + if (!File.Exists(cacheFilePath)) return; + + var pipeline = FunctionCodeDevEtl.Create(_connectionFactory, cacheFilePath); + + pipeline.ShouldNotBeNull(); + pipeline.PipelineName.ShouldBe("FunctionCode_Dev"); + } + + [Fact] + public async Task Execute_LoadsData() + { + var cacheFilePath = Path.Combine(_cacheDirectory, FunctionCodeDevEtl.CacheFileName); + if (!File.Exists(cacheFilePath)) return; + + var pipeline = FunctionCodeDevEtl.Create(_connectionFactory, cacheFilePath); + var result = await pipeline.ExecuteAsync(); + + result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed"); + result.TotalRows.ShouldBeGreaterThan(0); + + await using var connection = new SqlConnection(_connectionString); + await connection.OpenAsync(); + var count = await connection.ExecuteScalarAsync("SELECT COUNT(*) FROM dbo.FunctionCode"); + count.ShouldBe((int)result.TotalRows); + } + + [Fact] + public async Task Registry_RunAsync_LoadsTable() + { + if (!Directory.Exists(_cacheDirectory)) return; + + var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory); + var result = await registry.RunAsync("FunctionCode"); + + result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed"); + result.TotalRows.ShouldBeGreaterThan(0); + } + + [Fact] + public void Create_WithNullConnectionFactory_ThrowsArgumentNullException() + { + var cacheFilePath = Path.Combine(_cacheDirectory, FunctionCodeDevEtl.CacheFileName); + Should.Throw(() => FunctionCodeDevEtl.Create(null!, cacheFilePath)); + } +} diff --git a/NEW/tests/JdeScoping.DataSync.Dev.Tests/ItemDevEtlTests.cs b/NEW/tests/JdeScoping.DataSync.Dev.Tests/ItemDevEtlTests.cs new file mode 100644 index 0000000..a4a0b3e --- /dev/null +++ b/NEW/tests/JdeScoping.DataSync.Dev.Tests/ItemDevEtlTests.cs @@ -0,0 +1,96 @@ +using Dapper; +using JdeScoping.DataAccess; +using JdeScoping.DataAccess.Interfaces; +using JdeScoping.DataSync.Dev; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Abstractions; +using NSubstitute; +using Shouldly; + +namespace JdeScoping.DataSync.Dev.Tests; + +/// +/// Integration tests for Item development ETL. +/// +public class ItemDevEtlTests : IAsyncLifetime +{ + private readonly string _connectionString; + private readonly string _cacheDirectory; + private readonly IDbConnectionFactory _connectionFactory; + + public ItemDevEtlTests() + { + var config = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true) + .AddEnvironmentVariables() + .Build(); + + _connectionString = config.GetConnectionString("LotFinderDB") + ?? throw new InvalidOperationException("LotFinderDB connection string not configured."); + + _cacheDirectory = config["DevEtl:CacheDirectory"] + ?? Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "CACHED_DB_FILES"); + + _connectionFactory = new DbConnectionFactory(config, NullLogger.Instance); + } + + public async Task InitializeAsync() + { + await using var connection = new SqlConnection(_connectionString); + await connection.OpenAsync(); + await connection.ExecuteAsync("TRUNCATE TABLE dbo.Item"); + } + + public Task DisposeAsync() => Task.CompletedTask; + + [Fact] + public void Create_ReturnsValidPipeline() + { + var cacheFilePath = Path.Combine(_cacheDirectory, ItemDevEtl.CacheFileName); + if (!File.Exists(cacheFilePath)) return; + + var pipeline = ItemDevEtl.Create(_connectionFactory, cacheFilePath); + + pipeline.ShouldNotBeNull(); + pipeline.PipelineName.ShouldBe("Item_Dev"); + } + + [Fact] + public async Task Execute_LoadsData() + { + var cacheFilePath = Path.Combine(_cacheDirectory, ItemDevEtl.CacheFileName); + if (!File.Exists(cacheFilePath)) return; + + var pipeline = ItemDevEtl.Create(_connectionFactory, cacheFilePath); + var result = await pipeline.ExecuteAsync(); + + result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed"); + result.TotalRows.ShouldBeGreaterThan(0); + + await using var connection = new SqlConnection(_connectionString); + await connection.OpenAsync(); + var count = await connection.ExecuteScalarAsync("SELECT COUNT(*) FROM dbo.Item"); + count.ShouldBe((int)result.TotalRows); + } + + [Fact] + public async Task Registry_RunAsync_LoadsTable() + { + if (!Directory.Exists(_cacheDirectory)) return; + + var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory); + var result = await registry.RunAsync("Item"); + + result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed"); + result.TotalRows.ShouldBeGreaterThan(0); + } + + [Fact] + public void Create_WithNullConnectionFactory_ThrowsArgumentNullException() + { + var cacheFilePath = Path.Combine(_cacheDirectory, ItemDevEtl.CacheFileName); + Should.Throw(() => ItemDevEtl.Create(null!, cacheFilePath)); + } +} diff --git a/NEW/tests/JdeScoping.DataSync.Dev.Tests/JdeScoping.DataSync.Dev.Tests.csproj b/NEW/tests/JdeScoping.DataSync.Dev.Tests/JdeScoping.DataSync.Dev.Tests.csproj new file mode 100644 index 0000000..eba4d70 --- /dev/null +++ b/NEW/tests/JdeScoping.DataSync.Dev.Tests/JdeScoping.DataSync.Dev.Tests.csproj @@ -0,0 +1,44 @@ + + + + net10.0 + enable + enable + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + + + + + + diff --git a/NEW/tests/JdeScoping.DataSync.Dev.Tests/JdeUserDevEtlTests.cs b/NEW/tests/JdeScoping.DataSync.Dev.Tests/JdeUserDevEtlTests.cs new file mode 100644 index 0000000..7b780f0 --- /dev/null +++ b/NEW/tests/JdeScoping.DataSync.Dev.Tests/JdeUserDevEtlTests.cs @@ -0,0 +1,96 @@ +using Dapper; +using JdeScoping.DataAccess; +using JdeScoping.DataAccess.Interfaces; +using JdeScoping.DataSync.Dev; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Abstractions; +using NSubstitute; +using Shouldly; + +namespace JdeScoping.DataSync.Dev.Tests; + +/// +/// Integration tests for JdeUser development ETL. +/// +public class JdeUserDevEtlTests : IAsyncLifetime +{ + private readonly string _connectionString; + private readonly string _cacheDirectory; + private readonly IDbConnectionFactory _connectionFactory; + + public JdeUserDevEtlTests() + { + var config = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true) + .AddEnvironmentVariables() + .Build(); + + _connectionString = config.GetConnectionString("LotFinderDB") + ?? throw new InvalidOperationException("LotFinderDB connection string not configured."); + + _cacheDirectory = config["DevEtl:CacheDirectory"] + ?? Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "CACHED_DB_FILES"); + + _connectionFactory = new DbConnectionFactory(config, NullLogger.Instance); + } + + public async Task InitializeAsync() + { + await using var connection = new SqlConnection(_connectionString); + await connection.OpenAsync(); + await connection.ExecuteAsync("TRUNCATE TABLE dbo.JdeUser"); + } + + public Task DisposeAsync() => Task.CompletedTask; + + [Fact] + public void Create_ReturnsValidPipeline() + { + var cacheFilePath = Path.Combine(_cacheDirectory, JdeUserDevEtl.CacheFileName); + if (!File.Exists(cacheFilePath)) return; + + var pipeline = JdeUserDevEtl.Create(_connectionFactory, cacheFilePath); + + pipeline.ShouldNotBeNull(); + pipeline.PipelineName.ShouldBe("JdeUser_Dev"); + } + + [Fact] + public async Task Execute_LoadsData() + { + var cacheFilePath = Path.Combine(_cacheDirectory, JdeUserDevEtl.CacheFileName); + if (!File.Exists(cacheFilePath)) return; + + var pipeline = JdeUserDevEtl.Create(_connectionFactory, cacheFilePath); + var result = await pipeline.ExecuteAsync(); + + result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed"); + result.TotalRows.ShouldBeGreaterThan(0); + + await using var connection = new SqlConnection(_connectionString); + await connection.OpenAsync(); + var count = await connection.ExecuteScalarAsync("SELECT COUNT(*) FROM dbo.JdeUser"); + count.ShouldBe((int)result.TotalRows); + } + + [Fact] + public async Task Registry_RunAsync_LoadsTable() + { + if (!Directory.Exists(_cacheDirectory)) return; + + var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory); + var result = await registry.RunAsync("JdeUser"); + + result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed"); + result.TotalRows.ShouldBeGreaterThan(0); + } + + [Fact] + public void Create_WithNullConnectionFactory_ThrowsArgumentNullException() + { + var cacheFilePath = Path.Combine(_cacheDirectory, JdeUserDevEtl.CacheFileName); + Should.Throw(() => JdeUserDevEtl.Create(null!, cacheFilePath)); + } +} diff --git a/NEW/tests/JdeScoping.DataSync.Dev.Tests/OrgHierarchyDevEtlTests.cs b/NEW/tests/JdeScoping.DataSync.Dev.Tests/OrgHierarchyDevEtlTests.cs new file mode 100644 index 0000000..dc880b6 --- /dev/null +++ b/NEW/tests/JdeScoping.DataSync.Dev.Tests/OrgHierarchyDevEtlTests.cs @@ -0,0 +1,166 @@ +using Dapper; +using JdeScoping.DataAccess; +using JdeScoping.DataAccess.Interfaces; +using JdeScoping.DataSync.Dev; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Abstractions; +using NSubstitute; +using Shouldly; + +namespace JdeScoping.DataSync.Dev.Tests; + +/// +/// Integration tests for OrgHierarchy development ETL. +/// Requires: Local SQL Server, CACHED_DB_FILES directory with orghierarchy.json.zstd +/// +public class OrgHierarchyDevEtlTests : IAsyncLifetime +{ + private readonly string _connectionString; + private readonly string _cacheDirectory; + private readonly IDbConnectionFactory _connectionFactory; + + public OrgHierarchyDevEtlTests() + { + // Load configuration + var config = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true) + .AddEnvironmentVariables() + .Build(); + + _connectionString = config.GetConnectionString("LotFinderDB") + ?? throw new InvalidOperationException("LotFinderDB connection string not configured."); + + _cacheDirectory = config["DevEtl:CacheDirectory"] + ?? Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "CACHED_DB_FILES"); + + _connectionFactory = new DbConnectionFactory(config, NullLogger.Instance); + } + + public async Task InitializeAsync() + { + // Ensure OrgHierarchy table is empty before test + await using var connection = new SqlConnection(_connectionString); + await connection.OpenAsync(); + await connection.ExecuteAsync("TRUNCATE TABLE dbo.OrgHierarchy"); + } + + public Task DisposeAsync() => Task.CompletedTask; + + [Fact] + public void Create_ReturnsValidPipeline() + { + // Arrange + var cacheFilePath = Path.Combine(_cacheDirectory, OrgHierarchyDevEtl.CacheFileName); + if (!File.Exists(cacheFilePath)) + { + // Skip test if cache file doesn't exist + return; + } + + // Act + var pipeline = OrgHierarchyDevEtl.Create(_connectionFactory, cacheFilePath); + + // Assert + pipeline.ShouldNotBeNull(); + pipeline.PipelineName.ShouldBe("OrgHierarchy_Dev"); + } + + [Fact] + public async Task Execute_LoadsOrgHierarchyData() + { + // Arrange + var cacheFilePath = Path.Combine(_cacheDirectory, OrgHierarchyDevEtl.CacheFileName); + if (!File.Exists(cacheFilePath)) + { + // Skip test if cache file doesn't exist + return; + } + + var pipeline = OrgHierarchyDevEtl.Create(_connectionFactory, cacheFilePath); + + // Act + var result = await pipeline.ExecuteAsync(); + + // Assert + result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed"); + result.TotalRows.ShouldBeGreaterThan(0, "Should load at least one row"); + + // Verify data in database + await using var connection = new SqlConnection(_connectionString); + await connection.OpenAsync(); + var count = await connection.ExecuteScalarAsync("SELECT COUNT(*) FROM dbo.OrgHierarchy"); + + count.ShouldBe((int)result.TotalRows, "Database row count should match pipeline result"); + } + + [Fact] + public async Task Registry_RunAsync_LoadsOrgHierarchy() + { + // Arrange + if (!Directory.Exists(_cacheDirectory)) + { + // Skip test if cache directory doesn't exist + return; + } + + var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory); + + // Act + var result = await registry.RunAsync("OrgHierarchy"); + + // Assert + result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed"); + result.TotalRows.ShouldBeGreaterThan(0); + } + + [Fact] + public void Create_WithNullConnectionFactory_ThrowsArgumentNullException() + { + // Arrange + var cacheFilePath = Path.Combine(_cacheDirectory, OrgHierarchyDevEtl.CacheFileName); + + // Act & Assert + Should.Throw(() => OrgHierarchyDevEtl.Create(null!, cacheFilePath)); + } + + [Fact] + public void Create_WithEmptyCacheFilePath_ThrowsArgumentException() + { + // Arrange + var mockFactory = Substitute.For(); + + // Act & Assert + Should.Throw(() => OrgHierarchyDevEtl.Create(mockFactory, string.Empty)); + } + + [Fact] + public void Create_WithNonExistentCacheFile_ThrowsFileNotFoundException() + { + // Arrange + var mockFactory = Substitute.For(); + var nonExistentPath = "/nonexistent/path/orghierarchy.json.zstd"; + + // Act & Assert + Should.Throw(() => OrgHierarchyDevEtl.Create(mockFactory, nonExistentPath)); + } + + [Fact] + public void DevEtlRegistry_GetAvailableTables_IncludesOrgHierarchy() + { + // Arrange + if (!Directory.Exists(_cacheDirectory)) + { + return; + } + + var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory); + + // Act + var tables = registry.GetAvailableTables().ToList(); + + // Assert + tables.ShouldContain("OrgHierarchy"); + } +} diff --git a/NEW/tests/JdeScoping.DataSync.Dev.Tests/ProfitCenterDevEtlTests.cs b/NEW/tests/JdeScoping.DataSync.Dev.Tests/ProfitCenterDevEtlTests.cs new file mode 100644 index 0000000..2cc5c10 --- /dev/null +++ b/NEW/tests/JdeScoping.DataSync.Dev.Tests/ProfitCenterDevEtlTests.cs @@ -0,0 +1,117 @@ +using Dapper; +using JdeScoping.DataAccess; +using JdeScoping.DataAccess.Interfaces; +using JdeScoping.DataSync.Dev; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Abstractions; +using NSubstitute; +using Shouldly; + +namespace JdeScoping.DataSync.Dev.Tests; + +/// +/// Integration tests for ProfitCenter development ETL. +/// Requires: Local SQL Server, CACHED_DB_FILES directory with profitcenter.json.zstd +/// +public class ProfitCenterDevEtlTests : IAsyncLifetime +{ + private readonly string _connectionString; + private readonly string _cacheDirectory; + private readonly IDbConnectionFactory _connectionFactory; + + public ProfitCenterDevEtlTests() + { + var config = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true) + .AddEnvironmentVariables() + .Build(); + + _connectionString = config.GetConnectionString("LotFinderDB") + ?? throw new InvalidOperationException("LotFinderDB connection string not configured."); + + _cacheDirectory = config["DevEtl:CacheDirectory"] + ?? Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "CACHED_DB_FILES"); + + _connectionFactory = new DbConnectionFactory(config, NullLogger.Instance); + } + + public async Task InitializeAsync() + { + await using var connection = new SqlConnection(_connectionString); + await connection.OpenAsync(); + await connection.ExecuteAsync("TRUNCATE TABLE dbo.ProfitCenter"); + } + + public Task DisposeAsync() => Task.CompletedTask; + + [Fact] + public void Create_ReturnsValidPipeline() + { + var cacheFilePath = Path.Combine(_cacheDirectory, ProfitCenterDevEtl.CacheFileName); + if (!File.Exists(cacheFilePath)) return; + + var pipeline = ProfitCenterDevEtl.Create(_connectionFactory, cacheFilePath); + + pipeline.ShouldNotBeNull(); + pipeline.PipelineName.ShouldBe("ProfitCenter_Dev"); + } + + [Fact] + public async Task Execute_LoadsProfitCenterData() + { + var cacheFilePath = Path.Combine(_cacheDirectory, ProfitCenterDevEtl.CacheFileName); + if (!File.Exists(cacheFilePath)) return; + + var pipeline = ProfitCenterDevEtl.Create(_connectionFactory, cacheFilePath); + + var result = await pipeline.ExecuteAsync(); + + result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed"); + result.TotalRows.ShouldBeGreaterThan(0, "Should load at least one row"); + + await using var connection = new SqlConnection(_connectionString); + await connection.OpenAsync(); + var count = await connection.ExecuteScalarAsync("SELECT COUNT(*) FROM dbo.ProfitCenter"); + + count.ShouldBe((int)result.TotalRows); + } + + [Fact] + public async Task Registry_RunAsync_LoadsProfitCenter() + { + if (!Directory.Exists(_cacheDirectory)) return; + + var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory); + var result = await registry.RunAsync("ProfitCenter"); + + result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed"); + result.TotalRows.ShouldBeGreaterThan(0); + } + + [Fact] + public void Create_WithNullConnectionFactory_ThrowsArgumentNullException() + { + var cacheFilePath = Path.Combine(_cacheDirectory, ProfitCenterDevEtl.CacheFileName); + Should.Throw(() => ProfitCenterDevEtl.Create(null!, cacheFilePath)); + } + + [Fact] + public void Create_WithEmptyCacheFilePath_ThrowsArgumentException() + { + var mockFactory = Substitute.For(); + Should.Throw(() => ProfitCenterDevEtl.Create(mockFactory, string.Empty)); + } + + [Fact] + public void DevEtlRegistry_GetAvailableTables_IncludesProfitCenter() + { + if (!Directory.Exists(_cacheDirectory)) return; + + var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory); + var tables = registry.GetAvailableTables().ToList(); + + tables.ShouldContain("ProfitCenter"); + } +} diff --git a/NEW/tests/JdeScoping.DataSync.Dev.Tests/RouteMasterDevEtlTests.cs b/NEW/tests/JdeScoping.DataSync.Dev.Tests/RouteMasterDevEtlTests.cs new file mode 100644 index 0000000..c7fb45c --- /dev/null +++ b/NEW/tests/JdeScoping.DataSync.Dev.Tests/RouteMasterDevEtlTests.cs @@ -0,0 +1,96 @@ +using Dapper; +using JdeScoping.DataAccess; +using JdeScoping.DataAccess.Interfaces; +using JdeScoping.DataSync.Dev; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Abstractions; +using NSubstitute; +using Shouldly; + +namespace JdeScoping.DataSync.Dev.Tests; + +/// +/// Integration tests for RouteMaster development ETL. +/// +public class RouteMasterDevEtlTests : IAsyncLifetime +{ + private readonly string _connectionString; + private readonly string _cacheDirectory; + private readonly IDbConnectionFactory _connectionFactory; + + public RouteMasterDevEtlTests() + { + var config = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true) + .AddEnvironmentVariables() + .Build(); + + _connectionString = config.GetConnectionString("LotFinderDB") + ?? throw new InvalidOperationException("LotFinderDB connection string not configured."); + + _cacheDirectory = config["DevEtl:CacheDirectory"] + ?? Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "CACHED_DB_FILES"); + + _connectionFactory = new DbConnectionFactory(config, NullLogger.Instance); + } + + public async Task InitializeAsync() + { + await using var connection = new SqlConnection(_connectionString); + await connection.OpenAsync(); + await connection.ExecuteAsync("TRUNCATE TABLE dbo.RouteMaster"); + } + + public Task DisposeAsync() => Task.CompletedTask; + + [Fact] + public void Create_ReturnsValidPipeline() + { + var cacheFilePath = Path.Combine(_cacheDirectory, RouteMasterDevEtl.CacheFileName); + if (!File.Exists(cacheFilePath)) return; + + var pipeline = RouteMasterDevEtl.Create(_connectionFactory, cacheFilePath); + + pipeline.ShouldNotBeNull(); + pipeline.PipelineName.ShouldBe("RouteMaster_Dev"); + } + + [Fact] + public async Task Execute_LoadsData() + { + var cacheFilePath = Path.Combine(_cacheDirectory, RouteMasterDevEtl.CacheFileName); + if (!File.Exists(cacheFilePath)) return; + + var pipeline = RouteMasterDevEtl.Create(_connectionFactory, cacheFilePath); + var result = await pipeline.ExecuteAsync(); + + result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed"); + result.TotalRows.ShouldBeGreaterThan(0); + + await using var connection = new SqlConnection(_connectionString); + await connection.OpenAsync(); + var count = await connection.ExecuteScalarAsync("SELECT COUNT(*) FROM dbo.RouteMaster"); + count.ShouldBe((int)result.TotalRows); + } + + [Fact] + public async Task Registry_RunAsync_LoadsTable() + { + if (!Directory.Exists(_cacheDirectory)) return; + + var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory); + var result = await registry.RunAsync("RouteMaster"); + + result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed"); + result.TotalRows.ShouldBeGreaterThan(0); + } + + [Fact] + public void Create_WithNullConnectionFactory_ThrowsArgumentNullException() + { + var cacheFilePath = Path.Combine(_cacheDirectory, RouteMasterDevEtl.CacheFileName); + Should.Throw(() => RouteMasterDevEtl.Create(null!, cacheFilePath)); + } +} diff --git a/NEW/tests/JdeScoping.DataSync.Dev.Tests/WorkCenterDevEtlTests.cs b/NEW/tests/JdeScoping.DataSync.Dev.Tests/WorkCenterDevEtlTests.cs new file mode 100644 index 0000000..2b59d91 --- /dev/null +++ b/NEW/tests/JdeScoping.DataSync.Dev.Tests/WorkCenterDevEtlTests.cs @@ -0,0 +1,117 @@ +using Dapper; +using JdeScoping.DataAccess; +using JdeScoping.DataAccess.Interfaces; +using JdeScoping.DataSync.Dev; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Abstractions; +using NSubstitute; +using Shouldly; + +namespace JdeScoping.DataSync.Dev.Tests; + +/// +/// Integration tests for WorkCenter development ETL. +/// Requires: Local SQL Server, CACHED_DB_FILES directory with workcenter.json.zstd +/// +public class WorkCenterDevEtlTests : IAsyncLifetime +{ + private readonly string _connectionString; + private readonly string _cacheDirectory; + private readonly IDbConnectionFactory _connectionFactory; + + public WorkCenterDevEtlTests() + { + var config = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true) + .AddEnvironmentVariables() + .Build(); + + _connectionString = config.GetConnectionString("LotFinderDB") + ?? throw new InvalidOperationException("LotFinderDB connection string not configured."); + + _cacheDirectory = config["DevEtl:CacheDirectory"] + ?? Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "CACHED_DB_FILES"); + + _connectionFactory = new DbConnectionFactory(config, NullLogger.Instance); + } + + public async Task InitializeAsync() + { + await using var connection = new SqlConnection(_connectionString); + await connection.OpenAsync(); + await connection.ExecuteAsync("TRUNCATE TABLE dbo.WorkCenter"); + } + + public Task DisposeAsync() => Task.CompletedTask; + + [Fact] + public void Create_ReturnsValidPipeline() + { + var cacheFilePath = Path.Combine(_cacheDirectory, WorkCenterDevEtl.CacheFileName); + if (!File.Exists(cacheFilePath)) return; + + var pipeline = WorkCenterDevEtl.Create(_connectionFactory, cacheFilePath); + + pipeline.ShouldNotBeNull(); + pipeline.PipelineName.ShouldBe("WorkCenter_Dev"); + } + + [Fact] + public async Task Execute_LoadsWorkCenterData() + { + var cacheFilePath = Path.Combine(_cacheDirectory, WorkCenterDevEtl.CacheFileName); + if (!File.Exists(cacheFilePath)) return; + + var pipeline = WorkCenterDevEtl.Create(_connectionFactory, cacheFilePath); + + var result = await pipeline.ExecuteAsync(); + + result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed"); + result.TotalRows.ShouldBeGreaterThan(0, "Should load at least one row"); + + await using var connection = new SqlConnection(_connectionString); + await connection.OpenAsync(); + var count = await connection.ExecuteScalarAsync("SELECT COUNT(*) FROM dbo.WorkCenter"); + + count.ShouldBe((int)result.TotalRows); + } + + [Fact] + public async Task Registry_RunAsync_LoadsWorkCenter() + { + if (!Directory.Exists(_cacheDirectory)) return; + + var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory); + var result = await registry.RunAsync("WorkCenter"); + + result.Success.ShouldBeTrue(result.Error?.Message ?? "Pipeline should succeed"); + result.TotalRows.ShouldBeGreaterThan(0); + } + + [Fact] + public void Create_WithNullConnectionFactory_ThrowsArgumentNullException() + { + var cacheFilePath = Path.Combine(_cacheDirectory, WorkCenterDevEtl.CacheFileName); + Should.Throw(() => WorkCenterDevEtl.Create(null!, cacheFilePath)); + } + + [Fact] + public void Create_WithEmptyCacheFilePath_ThrowsArgumentException() + { + var mockFactory = Substitute.For(); + Should.Throw(() => WorkCenterDevEtl.Create(mockFactory, string.Empty)); + } + + [Fact] + public void DevEtlRegistry_GetAvailableTables_IncludesWorkCenter() + { + if (!Directory.Exists(_cacheDirectory)) return; + + var registry = new DevEtlRegistry(_connectionFactory, _cacheDirectory); + var tables = registry.GetAvailableTables().ToList(); + + tables.ShouldContain("WorkCenter"); + } +} diff --git a/NEW/tests/JdeScoping.DataSync.Dev.Tests/appsettings.json b/NEW/tests/JdeScoping.DataSync.Dev.Tests/appsettings.json new file mode 100644 index 0000000..3a01c2b --- /dev/null +++ b/NEW/tests/JdeScoping.DataSync.Dev.Tests/appsettings.json @@ -0,0 +1,8 @@ +{ + "ConnectionStrings": { + "LotFinderDB": "Server=localhost,1434;Database=ScopingTool;User Id=scopingapp;Password=Sc0ping@pp_Dev#2024;TrustServerCertificate=true" + }, + "DevEtl": { + "CacheDirectory": "/Users/dohertj2/Desktop/JdeScopingTool/CACHED_DB_FILES" + } +} diff --git a/PLANS/2026-01-06-datasync-dev-extraction-design.md b/PLANS/2026-01-06-datasync-dev-extraction-design.md new file mode 100644 index 0000000..e4858d5 --- /dev/null +++ b/PLANS/2026-01-06-datasync-dev-extraction-design.md @@ -0,0 +1,162 @@ +# Design: Extract DevEtl to JdeScoping.DataSync.Dev + +**Date:** 2026-01-06 +**Status:** Approved + +## Purpose + +Move dev/testing-specific ETL code from `JdeScoping.DataSync` into a separate `JdeScoping.DataSync.Dev` project. This code loads cached JSON/zstd files into SQL Server for local sandbox development and should not be part of the production DataSync assembly. + +## Scope + +### Files to Move (Source) + +**22 files** from `src/JdeScoping.DataSync/DevEtl/` → `src/JdeScoping.DataSync.Dev/` + +- `DevEtlRegistry.cs` +- `BranchDevEtl.cs` +- `FunctionCodeDevEtl.cs` +- `ItemDevEtl.cs` +- `JdeUserDevEtl.cs` +- `LotDevEtl.cs` +- `LotUsageCurrDevEtl.cs` +- `LotUsageHistDevEtl.cs` +- `MisDataDevEtl.cs` +- `OrgHierarchyDevEtl.cs` +- `ProfitCenterDevEtl.cs` +- `RouteMasterDevEtl.cs` +- `WorkCenterDevEtl.cs` +- `WorkOrderComponentCurrDevEtl.cs` +- `WorkOrderComponentHistDevEtl.cs` +- `WorkOrderCurrDevEtl.cs` +- `WorkOrderHistDevEtl.cs` +- `WorkOrderRoutingDevEtl.cs` +- `WorkOrderStepCurrDevEtl.cs` +- `WorkOrderStepHistDevEtl.cs` +- `WorkOrderTimeCurrDevEtl.cs` +- `WorkOrderTimeHistDevEtl.cs` + +### Files to Move (Tests) + +**8 files** from `tests/JdeScoping.DataSync.Tests/DevEtl/` → `tests/JdeScoping.DataSync.Dev.Tests/` + +- `BranchDevEtlTests.cs` +- `FunctionCodeDevEtlTests.cs` +- `ItemDevEtlTests.cs` +- `JdeUserDevEtlTests.cs` +- `OrgHierarchyDevEtlTests.cs` +- `ProfitCenterDevEtlTests.cs` +- `RouteMasterDevEtlTests.cs` +- `WorkCenterDevEtlTests.cs` + +## New Project Structure + +``` +NEW/ +├── src/ +│ └── JdeScoping.DataSync.Dev/ +│ ├── JdeScoping.DataSync.Dev.csproj +│ ├── DevEtlRegistry.cs +│ ├── BranchDevEtl.cs +│ └── ... (20 more files) +└── tests/ + └── JdeScoping.DataSync.Dev.Tests/ + ├── JdeScoping.DataSync.Dev.Tests.csproj + ├── BranchDevEtlTests.cs + └── ... (7 more files) +``` + +## Dependencies + +### JdeScoping.DataSync.Dev.csproj + +```xml + + + + net10.0 + enable + enable + + + + + + + +``` + +Reuses ETL infrastructure from DataSync: +- `EtlPipeline`, `EtlPipelineBuilder` +- `JsonZstdFileSource` +- `DbBulkImportDestination` +- `JsonColumnSchema` + +### JdeScoping.DataSync.Dev.Tests.csproj + +```xml + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + +``` + +## Namespace Changes + +All source files: +```csharp +// Before +namespace JdeScoping.DataSync.DevEtl; + +// After +namespace JdeScoping.DataSync.Dev; +``` + +All test files: +```csharp +// Before +using JdeScoping.DataSync.DevEtl; + +// After +using JdeScoping.DataSync.Dev; +``` + +## Cleanup + +After migration, delete: +- `src/JdeScoping.DataSync/DevEtl/` (entire folder) +- `tests/JdeScoping.DataSync.Tests/DevEtl/` (entire folder) + +## Cache Files + +No change to how cache files are located. The `DevEtlRegistry` continues to accept a `cacheDirectory` parameter at runtime. + +## Implementation Steps + +1. Create `src/JdeScoping.DataSync.Dev/JdeScoping.DataSync.Dev.csproj` +2. Move 22 source files from `DevEtl/` to new project +3. Update namespace in all source files +4. Create `tests/JdeScoping.DataSync.Dev.Tests/JdeScoping.DataSync.Dev.Tests.csproj` +5. Move 8 test files to new project +6. Update using statements in test files +7. Add both projects to solution +8. Delete old `DevEtl/` folders +9. Build and run tests to verify diff --git a/PLANS/2026-01-06-datasync-dev-extraction.md b/PLANS/2026-01-06-datasync-dev-extraction.md new file mode 100644 index 0000000..2b41c02 --- /dev/null +++ b/PLANS/2026-01-06-datasync-dev-extraction.md @@ -0,0 +1,389 @@ +# DataSync.Dev Extraction Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Extract dev/testing ETL code from JdeScoping.DataSync into a separate JdeScoping.DataSync.Dev project with its own test project. + +**Architecture:** Create two new projects (DataSync.Dev and DataSync.Dev.Tests) that depend on the existing DataSync project. Move all DevEtl files, update namespaces, update solution file, then delete original folders. + +**Tech Stack:** .NET 10, xUnit, Shouldly, NSubstitute + +--- + +## Task 1: Create JdeScoping.DataSync.Dev Project + +**Files:** +- Create: `NEW/src/JdeScoping.DataSync.Dev/JdeScoping.DataSync.Dev.csproj` + +**Step 1: Create project directory** + +Run: +```bash +mkdir -p /Users/dohertj2/Desktop/JdeScopingTool/NEW/src/JdeScoping.DataSync.Dev +``` + +**Step 2: Create project file** + +Create `NEW/src/JdeScoping.DataSync.Dev/JdeScoping.DataSync.Dev.csproj`: + +```xml + + + + net10.0 + enable + enable + + + + + + + +``` + +**Step 3: Verify project builds** + +Run: +```bash +dotnet build /Users/dohertj2/Desktop/JdeScopingTool/NEW/src/JdeScoping.DataSync.Dev/JdeScoping.DataSync.Dev.csproj +``` +Expected: Build succeeded + +--- + +## Task 2: Move Source Files to DataSync.Dev + +**Files:** +- Move: `NEW/src/JdeScoping.DataSync/DevEtl/*.cs` → `NEW/src/JdeScoping.DataSync.Dev/` + +**Step 1: Move all 22 source files** + +Run: +```bash +mv /Users/dohertj2/Desktop/JdeScopingTool/NEW/src/JdeScoping.DataSync/DevEtl/*.cs /Users/dohertj2/Desktop/JdeScopingTool/NEW/src/JdeScoping.DataSync.Dev/ +``` + +**Step 2: Remove empty DevEtl directory** + +Run: +```bash +rmdir /Users/dohertj2/Desktop/JdeScopingTool/NEW/src/JdeScoping.DataSync/DevEtl +``` + +**Step 3: Verify files moved** + +Run: +```bash +ls /Users/dohertj2/Desktop/JdeScopingTool/NEW/src/JdeScoping.DataSync.Dev/*.cs | wc -l +``` +Expected: 22 + +--- + +## Task 3: Update Namespaces in Source Files + +**Files:** +- Modify: All 22 `.cs` files in `NEW/src/JdeScoping.DataSync.Dev/` + +**Step 1: Update namespace declarations** + +Replace in all files: +```csharp +namespace JdeScoping.DataSync.DevEtl; +``` + +With: +```csharp +namespace JdeScoping.DataSync.Dev; +``` + +Run: +```bash +cd /Users/dohertj2/Desktop/JdeScopingTool/NEW/src/JdeScoping.DataSync.Dev && \ +sed -i '' 's/namespace JdeScoping\.DataSync\.DevEtl;/namespace JdeScoping.DataSync.Dev;/g' *.cs +``` + +**Step 2: Verify namespace changes** + +Run: +```bash +grep -l "JdeScoping.DataSync.DevEtl" /Users/dohertj2/Desktop/JdeScopingTool/NEW/src/JdeScoping.DataSync.Dev/*.cs +``` +Expected: No output (no files should contain old namespace) + +**Step 3: Verify new namespace exists** + +Run: +```bash +grep -c "namespace JdeScoping.DataSync.Dev;" /Users/dohertj2/Desktop/JdeScopingTool/NEW/src/JdeScoping.DataSync.Dev/*.cs | head -5 +``` +Expected: Each file shows `:1` + +**Step 4: Build to verify** + +Run: +```bash +dotnet build /Users/dohertj2/Desktop/JdeScopingTool/NEW/src/JdeScoping.DataSync.Dev/JdeScoping.DataSync.Dev.csproj +``` +Expected: Build succeeded + +--- + +## Task 4: Create JdeScoping.DataSync.Dev.Tests Project + +**Files:** +- Create: `NEW/tests/JdeScoping.DataSync.Dev.Tests/JdeScoping.DataSync.Dev.Tests.csproj` + +**Step 1: Create test project directory** + +Run: +```bash +mkdir -p /Users/dohertj2/Desktop/JdeScopingTool/NEW/tests/JdeScoping.DataSync.Dev.Tests +``` + +**Step 2: Create test project file** + +Create `NEW/tests/JdeScoping.DataSync.Dev.Tests/JdeScoping.DataSync.Dev.Tests.csproj`: + +```xml + + + + net10.0 + enable + enable + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + + + + + + +``` + +**Step 3: Copy appsettings.json from existing test project** + +Run: +```bash +cp /Users/dohertj2/Desktop/JdeScopingTool/NEW/tests/JdeScoping.DataSync.Tests/appsettings.json /Users/dohertj2/Desktop/JdeScopingTool/NEW/tests/JdeScoping.DataSync.Dev.Tests/ +``` + +**Step 4: Verify project builds** + +Run: +```bash +dotnet build /Users/dohertj2/Desktop/JdeScopingTool/NEW/tests/JdeScoping.DataSync.Dev.Tests/JdeScoping.DataSync.Dev.Tests.csproj +``` +Expected: Build succeeded + +--- + +## Task 5: Move Test Files to DataSync.Dev.Tests + +**Files:** +- Move: `NEW/tests/JdeScoping.DataSync.Tests/DevEtl/*.cs` → `NEW/tests/JdeScoping.DataSync.Dev.Tests/` + +**Step 1: Move all 8 test files** + +Run: +```bash +mv /Users/dohertj2/Desktop/JdeScopingTool/NEW/tests/JdeScoping.DataSync.Tests/DevEtl/*.cs /Users/dohertj2/Desktop/JdeScopingTool/NEW/tests/JdeScoping.DataSync.Dev.Tests/ +``` + +**Step 2: Remove empty DevEtl directory** + +Run: +```bash +rmdir /Users/dohertj2/Desktop/JdeScopingTool/NEW/tests/JdeScoping.DataSync.Tests/DevEtl +``` + +**Step 3: Verify files moved** + +Run: +```bash +ls /Users/dohertj2/Desktop/JdeScopingTool/NEW/tests/JdeScoping.DataSync.Dev.Tests/*.cs | wc -l +``` +Expected: 8 + +--- + +## Task 6: Update Namespaces and Usings in Test Files + +**Files:** +- Modify: All 8 `.cs` files in `NEW/tests/JdeScoping.DataSync.Dev.Tests/` + +**Step 1: Update namespace declarations** + +Replace in all files: +```csharp +namespace JdeScoping.DataSync.Tests.DevEtl; +``` + +With: +```csharp +namespace JdeScoping.DataSync.Dev.Tests; +``` + +Run: +```bash +cd /Users/dohertj2/Desktop/JdeScopingTool/NEW/tests/JdeScoping.DataSync.Dev.Tests && \ +sed -i '' 's/namespace JdeScoping\.DataSync\.Tests\.DevEtl;/namespace JdeScoping.DataSync.Dev.Tests;/g' *.cs +``` + +**Step 2: Update using statements** + +Replace in all files: +```csharp +using JdeScoping.DataSync.DevEtl; +``` + +With: +```csharp +using JdeScoping.DataSync.Dev; +``` + +Run: +```bash +cd /Users/dohertj2/Desktop/JdeScopingTool/NEW/tests/JdeScoping.DataSync.Dev.Tests && \ +sed -i '' 's/using JdeScoping\.DataSync\.DevEtl;/using JdeScoping.DataSync.Dev;/g' *.cs +``` + +**Step 3: Verify changes** + +Run: +```bash +grep -l "DevEtl" /Users/dohertj2/Desktop/JdeScopingTool/NEW/tests/JdeScoping.DataSync.Dev.Tests/*.cs +``` +Expected: No output (no files should contain "DevEtl") + +**Step 4: Build to verify** + +Run: +```bash +dotnet build /Users/dohertj2/Desktop/JdeScopingTool/NEW/tests/JdeScoping.DataSync.Dev.Tests/JdeScoping.DataSync.Dev.Tests.csproj +``` +Expected: Build succeeded + +--- + +## Task 7: Update Solution File + +**Files:** +- Modify: `NEW/JdeScoping.slnx` + +**Step 1: Add new projects to solution** + +Update `NEW/JdeScoping.slnx` to add two new Project entries: + +In the `/src/` folder section, add: +```xml + +``` + +In the `/tests/` folder section, add: +```xml + +``` + +**Step 2: Verify solution builds** + +Run: +```bash +dotnet build /Users/dohertj2/Desktop/JdeScopingTool/NEW/JdeScoping.slnx +``` +Expected: Build succeeded + +--- + +## Task 8: Run Tests to Verify + +**Step 1: Run new test project** + +Run: +```bash +dotnet test /Users/dohertj2/Desktop/JdeScopingTool/NEW/tests/JdeScoping.DataSync.Dev.Tests/JdeScoping.DataSync.Dev.Tests.csproj --verbosity normal +``` +Expected: All tests pass (some may skip if cache files don't exist) + +**Step 2: Run full solution tests** + +Run: +```bash +dotnet test /Users/dohertj2/Desktop/JdeScopingTool/NEW/JdeScoping.slnx --verbosity minimal +``` +Expected: All tests pass + +--- + +## Task 9: Commit Changes + +**Step 1: Stage all changes** + +Run: +```bash +cd /Users/dohertj2/Desktop/JdeScopingTool && \ +git add NEW/src/JdeScoping.DataSync.Dev/ && \ +git add NEW/tests/JdeScoping.DataSync.Dev.Tests/ && \ +git add NEW/JdeScoping.slnx +``` + +**Step 2: Commit** + +Run: +```bash +git commit -m "feat: extract DevEtl to JdeScoping.DataSync.Dev project + +- Create JdeScoping.DataSync.Dev for sandbox testing ETL code +- Create JdeScoping.DataSync.Dev.Tests for associated tests +- Move 22 source files and 8 test files +- Update namespaces from DevEtl to Dev +- Add both projects to solution" +``` + +--- + +## Summary + +| Task | Description | Files Changed | +|------|-------------|---------------| +| 1 | Create DataSync.Dev project | 1 new csproj | +| 2 | Move source files | 22 files moved | +| 3 | Update source namespaces | 22 files modified | +| 4 | Create DataSync.Dev.Tests project | 1 new csproj + appsettings | +| 5 | Move test files | 8 files moved | +| 6 | Update test namespaces/usings | 8 files modified | +| 7 | Update solution file | 1 file modified | +| 8 | Run tests | Verification | +| 9 | Commit | Git |