From 60744245243c788e8d1ff998bdd5aeed221d6981 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 6 Jan 2026 14:08:47 -0500 Subject: [PATCH] refactor(data-access): update ISearchQueryBuilder to use SearchId only - Change interface methods to accept int searchId instead of SearchModel - Update SqlKataSearchQueryBuilder to generate SQL using extraction functions - SQL now calls dbo.fn_GetSearchWorkOrders(@SearchId) etc instead of TVPs - Update SearchProcessor to pass model.Id to query builder - Update tests for new method signatures --- .../Interfaces/ISearchQueryBuilder.cs | 27 +- .../SqlKataSearchQueryBuilder.cs | 382 ++++++++++-------- .../Services/SearchProcessor.cs | 12 +- .../SqlKataSearchQueryBuilderTests.cs | 264 +++++------- 4 files changed, 341 insertions(+), 344 deletions(-) diff --git a/NEW/src/JdeScoping.DataAccess/Interfaces/ISearchQueryBuilder.cs b/NEW/src/JdeScoping.DataAccess/Interfaces/ISearchQueryBuilder.cs index b02fb1a..6fd46cc 100644 --- a/NEW/src/JdeScoping.DataAccess/Interfaces/ISearchQueryBuilder.cs +++ b/NEW/src/JdeScoping.DataAccess/Interfaces/ISearchQueryBuilder.cs @@ -3,28 +3,29 @@ using JdeScoping.DataAccess.Models; namespace JdeScoping.DataAccess.Interfaces; /// -/// Interface for building search queries using SqlKata. +/// Builds SQL queries for search operations. +/// Uses SQL extraction functions to retrieve criteria from the Search table. /// public interface ISearchQueryBuilder { /// - /// Builds the main search query for flagging and retrieving work orders. + /// Builds the main search query using extraction functions. /// - /// The search model containing filter criteria. - /// The compiled query result with SQL, parameters, and setup statements. - SearchQueryResult BuildSearchQuery(SearchModel model); + /// The search ID to extract criteria from. + /// Query result with SQL and parameters. + SearchQueryResult BuildSearchQuery(int searchId); /// - /// Builds the MIS data extraction query when ExtractMisData is enabled. + /// Builds the MIS data extraction query. /// - /// The search model containing filter criteria. - /// The compiled query result for MIS extraction. - SearchQueryResult BuildMisQuery(SearchModel model); + /// The search ID to extract criteria from. + /// Query result with SQL and parameters. + SearchQueryResult BuildMisQuery(int searchId); /// - /// Builds the MIS non-match query for work orders without MIS records. + /// Builds the MIS non-match extraction query. /// - /// The search model containing filter criteria. - /// The compiled query result for MIS non-match extraction. - SearchQueryResult BuildMisNonMatchQuery(SearchModel model); + /// The search ID. + /// Query result with SQL and parameters. + SearchQueryResult BuildMisNonMatchQuery(int searchId); } diff --git a/NEW/src/JdeScoping.DataAccess/QueryBuilders/SqlKataSearchQueryBuilder.cs b/NEW/src/JdeScoping.DataAccess/QueryBuilders/SqlKataSearchQueryBuilder.cs index c94d428..34969a3 100644 --- a/NEW/src/JdeScoping.DataAccess/QueryBuilders/SqlKataSearchQueryBuilder.cs +++ b/NEW/src/JdeScoping.DataAccess/QueryBuilders/SqlKataSearchQueryBuilder.cs @@ -1,4 +1,3 @@ -using JdeScoping.DataAccess.Extensions; using JdeScoping.DataAccess.Interfaces; using JdeScoping.DataAccess.Models; using SqlKata.Compilers; @@ -7,83 +6,75 @@ namespace JdeScoping.DataAccess.QueryBuilders; /// /// SqlKata-based implementation of ISearchQueryBuilder. -/// Builds SQL queries using a fluent query builder pattern. +/// Generates SQL that uses extraction functions to retrieve criteria from Search.Criteria JSON. /// public sealed class SqlKataSearchQueryBuilder : ISearchQueryBuilder { private readonly SqlServerCompiler _compiler; - private readonly IEnumerable _filterHandlers; /// /// Initializes a new instance of SqlKataSearchQueryBuilder. /// /// The SqlKata SQL Server compiler. - /// Collection of filter handlers. - public SqlKataSearchQueryBuilder( - SqlServerCompiler compiler, - IEnumerable filterHandlers) + public SqlKataSearchQueryBuilder(SqlServerCompiler compiler) { _compiler = compiler; - _filterHandlers = filterHandlers.OrderBy(h => h.Priority); } /// - public SearchQueryResult BuildSearchQuery(SearchModel model) + public SearchQueryResult BuildSearchQuery(int searchId) { var setupStatements = new List(); - var parameters = new Dictionary(); + var parameters = new Dictionary + { + ["SearchId"] = searchId + }; - // 1. Create the #Temp_WO temp table + // 1. Validate the search criteria + setupStatements.Add("EXEC dbo.usp_ValidateSearchCriteria @SearchId;"); + + // 2. Create the #Temp_WO temp table setupStatements.Add(BuildTempWoTableSql()); - // 2. Apply filter handlers in priority order - foreach (var handler in _filterHandlers.Where(h => h.IsEnabled(model))) - { - var filterResult = handler.Apply(model, _compiler); - setupStatements.AddRange(filterResult.SetupSql); - foreach (var param in filterResult.Parameters) - { - parameters[param.Key] = param.Value; - } - } + // 3. Create filter temp tables and populate from extraction functions + setupStatements.Add(BuildWorkOrderFilterSetup()); + setupStatements.Add(BuildItemNumberFilterSetup()); + setupStatements.Add(BuildWorkCenterFilterSetup()); + setupStatements.Add(BuildOperatorFilterSetup()); + setupStatements.Add(BuildComponentLotFilterSetup()); + setupStatements.Add(BuildPartOperationsFilterSetup()); - // 3. Build step-based flagging query if needed - if (model.ShouldSearchSteps()) - { - setupStatements.Add(BuildStepFlaggingQuery(model)); - } + // 4. Build step-based flagging query + setupStatements.Add(BuildStepFlaggingQuery()); - // 4. Build the final result SELECT query + // 5. Build the final result SELECT query var resultSql = BuildResultQuery(); return new SearchQueryResult(resultSql, parameters, setupStatements); } /// - public SearchQueryResult BuildMisQuery(SearchModel model) + public SearchQueryResult BuildMisQuery(int searchId) { - // MIS query is delegated to MisQueryBuilder - // This is a placeholder - full implementation would generate MIS extraction SQL - var parameters = new Dictionary(); - - if (model.MinimumDt.HasValue) + var parameters = new Dictionary { - parameters["p_MinimumDT"] = model.MinimumDt.Value; - } - if (model.MaximumDt.HasValue) - { - parameters["p_MaximumDT"] = model.MaximumDt.Value; - } + ["SearchId"] = searchId + }; - var sql = BuildMisExtractionQuery(model); + var sql = BuildMisExtractionQuery(); return new SearchQueryResult(sql, parameters, []); } /// - public SearchQueryResult BuildMisNonMatchQuery(SearchModel model) + public SearchQueryResult BuildMisNonMatchQuery(int searchId) { + var parameters = new Dictionary + { + ["SearchId"] = searchId + }; + var sql = BuildMisNonMatchExtractionQuery(); - return new SearchQueryResult(sql, new Dictionary(), []); + return new SearchQueryResult(sql, parameters, []); } private static string BuildTempWoTableSql() @@ -110,147 +101,204 @@ public sealed class SqlKataSearchQueryBuilder : ISearchQueryBuilder """; } - private string BuildStepFlaggingQuery(SearchModel model) + private static string BuildWorkOrderFilterSetup() { - // Build the complex step-based flagging query dynamically - var queryParts = new List(); + return """ + --Setup work order filter from extraction function + IF OBJECT_ID('tempdb.dbo.#P_WorkOrders', 'U') IS NOT NULL + DROP TABLE #P_WorkOrders; + CREATE TABLE #P_WorkOrders (WorkOrderNumber BIGINT NOT NULL PRIMARY KEY); + INSERT INTO #P_WorkOrders (WorkOrderNumber) + SELECT WorkOrderNumber FROM dbo.fn_GetSearchWorkOrders(@SearchId); - // First query part - WorkOrderStep based - queryParts.Add(BuildStepSubquery(model, includeWorkOrderTime: true)); + --Add manually specified work order numbers to flagged list + IF EXISTS (SELECT 1 FROM #P_WorkOrders) + BEGIN + WITH WOP_CTE AS( + SELECT DISTINCT wo.WorkOrderNumber, + wo.LotNumber, + wo.BranchCode, + wo.ShortItemNumber + FROM dbo.WorkOrder AS wo INNER JOIN + #P_WorkOrders AS pwof ON (wo.WorkOrderNumber = pwof.WorkOrderNumber) + ) + MERGE #Temp_WO AS TARGET + USING WOP_CTE AS SOURCE + ON (TARGET.WorkOrderNumber = SOURCE.WorkOrderNumber) + WHEN MATCHED THEN + UPDATE SET TARGET.ManuallySpecified = 1 + WHEN NOT MATCHED BY TARGET THEN + INSERT(WorkOrderNumber, LotNumber, BranchCode, ShortItemNumber, ManuallySpecified) + VALUES(SOURCE.WorkOrderNumber, SOURCE.LotNumber, SOURCE.BranchCode, SOURCE.ShortItemNumber, 1); - // Second query part - WorkOrderTime only (if not filtering by MIS) - if (!model.ItemOperationMisFilterEnabled) - { - queryParts.Add(BuildTimeOnlySubquery(model)); - } - - var unionQuery = string.Join("\r\n UNION \r\n", queryParts); - - return $""" - --Query data - WITH LU_WO AS( - SELECT DISTINCT - step.WorkOrderNumber, - step.LotNumber, - step.BranchCode, - step.ShortItemNumber - FROM ( - {unionQuery} - ) step - ) - MERGE INTO #Temp_WO AS TARGET - USING LU_WO AS SOURCE - ON (TARGET.WorkOrderNumber = SOURCE.WorkOrderNumber AND TARGET.BranchCode = SOURCE.BranchCode) - WHEN MATCHED THEN - UPDATE SET TARGET.Flagged = 1 - WHEN NOT MATCHED BY TARGET THEN - INSERT(WorkOrderNumber, LotNumber, BranchCode, ShortItemNumber, Flagged) - VALUES(SOURCE.WorkOrderNumber, SOURCE.LotNumber, SOURCE.BranchCode, SOURCE.ShortItemNumber, 1); + --Add any work orders split from flagged work orders + WITH SP_WO AS + ( + SELECT DISTINCT wo.WorkOrderNumber, + wo.LotNumber, + wo.BranchCode, + wo.ShortItemNumber + FROM dbo.WorkOrder AS wo INNER JOIN + #Temp_WO AS tw_o ON (wo.ParentWorkOrderNumber = CAST(tw_o.WorkOrderNumber AS VARCHAR(8)) AND wo.BranchCode = tw_o.BranchCode) + ) + MERGE #Temp_WO AS TARGET + USING SP_WO AS SOURCE + ON (TARGET.WorkOrderNumber = SOURCE.WorkOrderNumber AND TARGET.BranchCode = SOURCE.BranchCode) + WHEN MATCHED THEN + UPDATE SET TARGET.SplitOrder = 1 + WHEN NOT MATCHED BY TARGET THEN + INSERT (WorkOrderNumber, LotNumber, BranchCode, ShortItemNumber, SplitOrder) + VALUES (SOURCE.WorkOrderNumber, SOURCE.LotNumber, SOURCE.BranchCode, SOURCE.ShortItemNumber, 1); + END """; } - private string BuildStepSubquery(SearchModel model, bool includeWorkOrderTime) + private static string BuildItemNumberFilterSetup() { - var joins = new List(); - var whereClause = ""; - - // Item number filter join - if (model.ItemNumberFilterEnabled) - { - joins.Add(" #P_ItemNumbers p_in ON (wo.ItemNumber = p_in.ItemNumber) INNER JOIN"); - } - - // Base WorkOrderStep join - joins.Add(" dbo.WorkOrderStep wos ON (wo.WorkOrderNumber = wos.WorkOrderNumber AND wo.BranchCode = wos.BranchCode) LEFT OUTER JOIN"); - joins.Add(" dbo.WorkOrderTime wot ON (wos.WorkOrderNumber = wot.WorkOrderNumber AND wos.StepNumber = wot.StepNumber AND wos.BranchCode = wot.BranchCode)"); - - // Work center filter join - if (model.ProfitCenterFilterEnabled || model.WorkCenterFilterEnabled) - { - joins.Add(" INNER JOIN \r\n #P_WorkCenters p_wc ON (wos.WorkCenterCode = p_wc.Code)"); - } - - // Operator filter join - if (model.OperatorFilterEnabled) - { - joins.Add(" INNER JOIN \r\n #P_OperatorIDs p_oi ON (wot.AddressNumber = p_oi.AddressNumber)"); - } - - // MIS filter join (uses CROSS APPLY) - if (model.ItemOperationMisFilterEnabled) - { - joins.Add(""" - CROSS APPLY - dbo.MatchMIS(wo.WorkOrderNumber, wo.ItemNumber, wo.BranchCode, wo.RoutingType, wo.IssueDate, wos.WorkCenterCode, - wos.StepNumber, wos.EndDT, wos.FunctionCode, wos.FunctionOperationDescription) AS mm INNER JOIN - #P_PartOperations p_po on (mm.ItemNumber = p_po.ItemNumber AND mm.MisSequenceNumber = p_po.OperationNumber AND mm.MisNumber = p_po.MisNumber AND mm.RevID = p_po.MisRevision) - """); - } - - // Timespan WHERE clause - if (model.TimespanFilterEnabled) - { - whereClause = """ - WHERE (wos.EndDT <= @p_MaximumDT AND wos.EndDT >= @p_MinimumDT) OR - (wot.GlDate <= @p_MaximumDT AND wot.GlDate >= @p_MinimumDT) - """; - } - - return $""" - SELECT DISTINCT - wo.WorkOrderNumber, - COALESCE(wo.LotNumber, CAST(wo.WorkOrderNumber AS VARCHAR(8))) AS LotNumber, - wo.BranchCode, - wo.ShortItemNumber - FROM dbo.WorkOrder wo INNER JOIN - {string.Join("\r\n", joins)} - {whereClause} + return """ + --Setup item number filter from extraction function + IF OBJECT_ID('tempdb.dbo.#P_ItemNumbers', 'U') IS NOT NULL + DROP TABLE #P_ItemNumbers; + CREATE TABLE #P_ItemNumbers (ItemNumber VARCHAR(128) NOT NULL PRIMARY KEY); + INSERT INTO #P_ItemNumbers (ItemNumber) + SELECT ItemNumber FROM dbo.fn_GetSearchItemNumbers(@SearchId); """; } - private string BuildTimeOnlySubquery(SearchModel model) + private static string BuildWorkCenterFilterSetup() { - var joins = new List(); - var whereClause = ""; + return """ + --Setup work center filter from extraction functions (combines profit centers and work centers) + IF OBJECT_ID('tempdb.dbo.#P_WorkCenters', 'U') IS NOT NULL + DROP TABLE #P_WorkCenters; + CREATE TABLE #P_WorkCenters (Code VARCHAR(12) NOT NULL PRIMARY KEY); - // Item number filter join - if (model.ItemNumberFilterEnabled) - { - joins.Add(" #P_ItemNumbers p_in ON (wo.ItemNumber = p_in.ItemNumber) INNER JOIN"); - } + --Insert from profit centers (joined with WorkCenter table) + INSERT INTO #P_WorkCenters (Code) + SELECT DISTINCT wc.WorkCenterCode + FROM dbo.fn_GetSearchProfitCenters(@SearchId) pc + INNER JOIN dbo.WorkCenter wc ON pc.Code = wc.ProfitCenterCode + WHERE NOT EXISTS (SELECT 1 FROM #P_WorkCenters WHERE Code = wc.WorkCenterCode); - // Base WorkOrderTime join - joins.Add(" dbo.WorkOrderTime wot ON (wo.WorkOrderNumber = wot.WorkOrderNumber AND wo.BranchCode = wot.BranchCode)"); + --Insert from work centers directly + INSERT INTO #P_WorkCenters (Code) + SELECT Code + FROM dbo.fn_GetSearchWorkCenters(@SearchId) + WHERE NOT EXISTS (SELECT 1 FROM #P_WorkCenters WHERE Code = Code); + """; + } - // Work center filter join - if (model.ProfitCenterFilterEnabled || model.WorkCenterFilterEnabled) - { - joins.Add(" INNER JOIN \r\n #P_WorkCenters p_wc ON (wot.WorkCenterCode = p_wc.Code)"); - } + private static string BuildOperatorFilterSetup() + { + return """ + --Setup operator filter from extraction function + IF OBJECT_ID('tempdb.dbo.#P_OperatorIDs', 'U') IS NOT NULL + DROP TABLE #P_OperatorIDs; + CREATE TABLE #P_OperatorIDs (AddressNumber BIGINT NOT NULL PRIMARY KEY); + INSERT INTO #P_OperatorIDs (AddressNumber) + SELECT DISTINCT u.AddressNumber + FROM dbo.fn_GetSearchOperatorIDs(@SearchId) op + INNER JOIN dbo.JdeUser u ON op.OperatorID = u.UserId; + """; + } - // Operator filter join - if (model.OperatorFilterEnabled) - { - joins.Add(" INNER JOIN \r\n #P_OperatorIDs p_oi ON (wot.AddressNumber = p_oi.AddressNumber)"); - } + private static string BuildComponentLotFilterSetup() + { + return """ + --Setup component lot filter from extraction function + IF OBJECT_ID('tempdb.dbo.#P_ComponentLots', 'U') IS NOT NULL + DROP TABLE #P_ComponentLots; + CREATE TABLE #P_ComponentLots ( + LotNumber VARCHAR(30) NOT NULL, + ItemNumber VARCHAR(128) NOT NULL, + PRIMARY KEY (LotNumber, ItemNumber) + ); + INSERT INTO #P_ComponentLots (LotNumber, ItemNumber) + SELECT LotNumber, ItemNumber FROM dbo.fn_GetSearchComponentLots(@SearchId); + """; + } - // Timespan WHERE clause (only if both min and max are present) - if (model.MinimumDt.HasValue && model.MaximumDt.HasValue) - { - whereClause = """ - WHERE (wot.GlDate <= @p_MaximumDT AND wot.GlDate >= @p_MinimumDT) - """; - } + private static string BuildPartOperationsFilterSetup() + { + return """ + --Setup part operations filter from extraction function + IF OBJECT_ID('tempdb.dbo.#P_PartOperations', 'U') IS NOT NULL + DROP TABLE #P_PartOperations; + CREATE TABLE #P_PartOperations ( + ItemNumber VARCHAR(128) NOT NULL, + OperationNumber VARCHAR(10) NOT NULL, + MisNumber VARCHAR(10) NULL, + MisRevision VARCHAR(10) NULL, + PRIMARY KEY (ItemNumber, OperationNumber) + ); + INSERT INTO #P_PartOperations (ItemNumber, OperationNumber, MisNumber, MisRevision) + SELECT ItemNumber, OperationNumber, MisNumber, MisRevision + FROM dbo.fn_GetSearchPartOperations(@SearchId); + """; + } - return $""" - SELECT DISTINCT - wo.WorkOrderNumber, - COALESCE(wo.LotNumber, CAST(wo.WorkOrderNumber AS VARCHAR(8))) AS LotNumber, - wo.BranchCode, - wo.ShortItemNumber - FROM dbo.WorkOrder wo INNER JOIN - {string.Join("\r\n", joins)} - {whereClause} + private static string BuildStepFlaggingQuery() + { + // Build the complex step-based flagging query dynamically using extraction functions + return """ + --Query data using extraction functions for date filtering + DECLARE @MinDt DATETIME2(7) = dbo.fn_GetSearchMinimumDt(@SearchId); + DECLARE @MaxDt DATETIME2(7) = dbo.fn_GetSearchMaximumDt(@SearchId); + DECLARE @HasItemFilter BIT = CASE WHEN EXISTS(SELECT 1 FROM #P_ItemNumbers) THEN 1 ELSE 0 END; + DECLARE @HasWorkCenterFilter BIT = CASE WHEN EXISTS(SELECT 1 FROM #P_WorkCenters) THEN 1 ELSE 0 END; + DECLARE @HasOperatorFilter BIT = CASE WHEN EXISTS(SELECT 1 FROM #P_OperatorIDs) THEN 1 ELSE 0 END; + DECLARE @HasPartOpsFilter BIT = CASE WHEN EXISTS(SELECT 1 FROM #P_PartOperations) THEN 1 ELSE 0 END; + DECLARE @HasTimespanFilter BIT = CASE WHEN @MinDt IS NOT NULL OR @MaxDt IS NOT NULL THEN 1 ELSE 0 END; + DECLARE @ShouldSearchSteps BIT = CASE WHEN @HasTimespanFilter = 1 OR @HasWorkCenterFilter = 1 OR @HasOperatorFilter = 1 THEN 1 ELSE 0 END; + + IF @ShouldSearchSteps = 1 + BEGIN + WITH LU_WO AS( + SELECT DISTINCT + wo.WorkOrderNumber, + COALESCE(wo.LotNumber, CAST(wo.WorkOrderNumber AS VARCHAR(8))) AS LotNumber, + wo.BranchCode, + wo.ShortItemNumber + FROM dbo.WorkOrder wo + LEFT OUTER JOIN #P_ItemNumbers p_in ON (@HasItemFilter = 0 OR wo.ItemNumber = p_in.ItemNumber) + INNER JOIN dbo.WorkOrderStep wos ON (wo.WorkOrderNumber = wos.WorkOrderNumber AND wo.BranchCode = wos.BranchCode) + LEFT OUTER JOIN dbo.WorkOrderTime wot ON (wos.WorkOrderNumber = wot.WorkOrderNumber AND wos.StepNumber = wot.StepNumber AND wos.BranchCode = wot.BranchCode) + LEFT OUTER JOIN #P_WorkCenters p_wc ON (@HasWorkCenterFilter = 0 OR wos.WorkCenterCode = p_wc.Code) + LEFT OUTER JOIN #P_OperatorIDs p_oi ON (@HasOperatorFilter = 0 OR wot.AddressNumber = p_oi.AddressNumber) + WHERE (@HasItemFilter = 0 OR p_in.ItemNumber IS NOT NULL) + AND (@HasWorkCenterFilter = 0 OR p_wc.Code IS NOT NULL) + AND (@HasOperatorFilter = 0 OR p_oi.AddressNumber IS NOT NULL) + AND (@HasTimespanFilter = 0 OR + (wos.EndDT <= @MaxDt AND wos.EndDT >= @MinDt) OR + (wot.GlDate <= @MaxDt AND wot.GlDate >= @MinDt)) + + UNION + + SELECT DISTINCT + wo.WorkOrderNumber, + COALESCE(wo.LotNumber, CAST(wo.WorkOrderNumber AS VARCHAR(8))) AS LotNumber, + wo.BranchCode, + wo.ShortItemNumber + FROM dbo.WorkOrder wo + LEFT OUTER JOIN #P_ItemNumbers p_in ON (@HasItemFilter = 0 OR wo.ItemNumber = p_in.ItemNumber) + INNER JOIN dbo.WorkOrderTime wot ON (wo.WorkOrderNumber = wot.WorkOrderNumber AND wo.BranchCode = wot.BranchCode) + LEFT OUTER JOIN #P_WorkCenters p_wc ON (@HasWorkCenterFilter = 0 OR wot.WorkCenterCode = p_wc.Code) + LEFT OUTER JOIN #P_OperatorIDs p_oi ON (@HasOperatorFilter = 0 OR wot.AddressNumber = p_oi.AddressNumber) + WHERE (@HasItemFilter = 0 OR p_in.ItemNumber IS NOT NULL) + AND (@HasWorkCenterFilter = 0 OR p_wc.Code IS NOT NULL) + AND (@HasOperatorFilter = 0 OR p_oi.AddressNumber IS NOT NULL) + AND @HasPartOpsFilter = 0 + AND (@HasTimespanFilter = 0 OR (wot.GlDate <= @MaxDt AND wot.GlDate >= @MinDt)) + ) + MERGE INTO #Temp_WO AS TARGET + USING LU_WO AS SOURCE + ON (TARGET.WorkOrderNumber = SOURCE.WorkOrderNumber AND TARGET.BranchCode = SOURCE.BranchCode) + WHEN MATCHED THEN + UPDATE SET TARGET.Flagged = 1 + WHEN NOT MATCHED BY TARGET THEN + INSERT(WorkOrderNumber, LotNumber, BranchCode, ShortItemNumber, Flagged) + VALUES(SOURCE.WorkOrderNumber, SOURCE.LotNumber, SOURCE.BranchCode, SOURCE.ShortItemNumber, 1); + END """; } @@ -304,7 +352,7 @@ public sealed class SqlKataSearchQueryBuilder : ISearchQueryBuilder """; } - private static string BuildMisExtractionQuery(SearchModel model) + private static string BuildMisExtractionQuery() { return """ --Get MIS search results diff --git a/NEW/src/JdeScoping.DataAccess/Services/SearchProcessor.cs b/NEW/src/JdeScoping.DataAccess/Services/SearchProcessor.cs index 6c3c9ae..740ec09 100644 --- a/NEW/src/JdeScoping.DataAccess/Services/SearchProcessor.cs +++ b/NEW/src/JdeScoping.DataAccess/Services/SearchProcessor.cs @@ -57,8 +57,8 @@ public sealed class SearchProcessor await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct); - // Build the search query - var queryResult = _queryBuilder.BuildSearchQuery(model); + // Build the search query using searchId only + var queryResult = _queryBuilder.BuildSearchQuery(model.Id); if (_options.EnableDebugSql && !string.IsNullOrEmpty(_options.DebugSqlPath)) { @@ -111,8 +111,8 @@ public sealed class SearchProcessor await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct); - // Build the search query - var queryResult = _queryBuilder.BuildSearchQuery(model); + // Build the search query using searchId only + var queryResult = _queryBuilder.BuildSearchQuery(model.Id); if (_options.EnableDebugSql && !string.IsNullOrEmpty(_options.DebugSqlPath)) { @@ -183,7 +183,7 @@ public sealed class SearchProcessor } // Execute MIS result query - var misQueryResult = _queryBuilder.BuildMisQuery(model); + var misQueryResult = _queryBuilder.BuildMisQuery(model.Id); var misResults = await connection.QueryAsync( misQueryResult.Sql, misQueryResult.Parameters, @@ -193,7 +193,7 @@ public sealed class SearchProcessor _logger.LogDebug("Found {MisResultCount} MIS results", model.MisResults.Count); // Execute MIS non-match query - var misNonMatchQueryResult = _queryBuilder.BuildMisNonMatchQuery(model); + var misNonMatchQueryResult = _queryBuilder.BuildMisNonMatchQuery(model.Id); var misNonMatchResults = await connection.QueryAsync( misNonMatchQueryResult.Sql, misNonMatchQueryResult.Parameters, diff --git a/NEW/tests/JdeScoping.DataAccess.Tests/QueryBuilders/SqlKataSearchQueryBuilderTests.cs b/NEW/tests/JdeScoping.DataAccess.Tests/QueryBuilders/SqlKataSearchQueryBuilderTests.cs index 6d5c724..c479684 100644 --- a/NEW/tests/JdeScoping.DataAccess.Tests/QueryBuilders/SqlKataSearchQueryBuilderTests.cs +++ b/NEW/tests/JdeScoping.DataAccess.Tests/QueryBuilders/SqlKataSearchQueryBuilderTests.cs @@ -1,9 +1,5 @@ -using JdeScoping.DataAccess.FilterHandlers; using JdeScoping.DataAccess.Interfaces; -using JdeScoping.DataAccess.Models; -using JdeScoping.DataAccess.Models.FilterEntries; using JdeScoping.DataAccess.QueryBuilders; -using NSubstitute; using Shouldly; using SqlKata.Compilers; using Xunit; @@ -18,221 +14,173 @@ public sealed class SqlKataSearchQueryBuilderTests private readonly SqlServerCompiler _compiler = new(); [Fact] - public void BuildSearchQuery_WithEmptyFilters_ProducesMinimalQuery() + public void BuildSearchQuery_WithSearchId_ProducesValidQuery() { // Arrange - var handlers = Array.Empty(); - var builder = new SqlKataSearchQueryBuilder(_compiler, handlers); - var model = new SearchModel(); + var builder = new SqlKataSearchQueryBuilder(_compiler); + var searchId = 123; // Act - var result = builder.BuildSearchQuery(model); + var result = builder.BuildSearchQuery(searchId); // Assert result.ShouldNotBeNull(); result.Sql.ShouldNotBeNullOrEmpty(); result.TempTableSetupSql.ShouldNotBeEmpty(); + result.Parameters.ShouldContainKey("SearchId"); + result.Parameters["SearchId"].ShouldBe(searchId); + } - // Should contain temp table creation + [Fact] + public void BuildSearchQuery_ContainsTempTableCreation() + { + // Arrange + var builder = new SqlKataSearchQueryBuilder(_compiler); + + // Act + var result = builder.BuildSearchQuery(1); + + // Assert var setupSql = string.Join("\n", result.TempTableSetupSql); setupSql.ShouldContain("#Temp_WO"); setupSql.ShouldContain("CREATE TABLE"); } [Fact] - public void BuildSearchQuery_WithEmptyFilters_ResultSqlContainsSelect() + public void BuildSearchQuery_ContainsValidationCall() { // Arrange - var handlers = Array.Empty(); - var builder = new SqlKataSearchQueryBuilder(_compiler, handlers); - var model = new SearchModel(); + var builder = new SqlKataSearchQueryBuilder(_compiler); // Act - var result = builder.BuildSearchQuery(model); + var result = builder.BuildSearchQuery(1); + + // Assert + var setupSql = string.Join("\n", result.TempTableSetupSql); + setupSql.ShouldContain("usp_ValidateSearchCriteria"); + setupSql.ShouldContain("@SearchId"); + } + + [Fact] + public void BuildSearchQuery_UsesExtractionFunctions() + { + // Arrange + var builder = new SqlKataSearchQueryBuilder(_compiler); + + // Act + var result = builder.BuildSearchQuery(1); + + // Assert + var setupSql = string.Join("\n", result.TempTableSetupSql); + + // Should use extraction functions instead of TVPs + setupSql.ShouldContain("fn_GetSearchWorkOrders(@SearchId)"); + setupSql.ShouldContain("fn_GetSearchItemNumbers(@SearchId)"); + setupSql.ShouldContain("fn_GetSearchProfitCenters(@SearchId)"); + setupSql.ShouldContain("fn_GetSearchWorkCenters(@SearchId)"); + setupSql.ShouldContain("fn_GetSearchOperatorIDs(@SearchId)"); + setupSql.ShouldContain("fn_GetSearchComponentLots(@SearchId)"); + setupSql.ShouldContain("fn_GetSearchPartOperations(@SearchId)"); + setupSql.ShouldContain("fn_GetSearchMinimumDt(@SearchId)"); + setupSql.ShouldContain("fn_GetSearchMaximumDt(@SearchId)"); + } + + [Fact] + public void BuildSearchQuery_CreatesFilterTempTables() + { + // Arrange + var builder = new SqlKataSearchQueryBuilder(_compiler); + + // Act + var result = builder.BuildSearchQuery(1); + + // Assert + var setupSql = string.Join("\n", result.TempTableSetupSql); + + // Should create filter temp tables + setupSql.ShouldContain("#P_WorkOrders"); + setupSql.ShouldContain("#P_ItemNumbers"); + setupSql.ShouldContain("#P_WorkCenters"); + setupSql.ShouldContain("#P_OperatorIDs"); + setupSql.ShouldContain("#P_ComponentLots"); + setupSql.ShouldContain("#P_PartOperations"); + } + + [Fact] + public void BuildSearchQuery_ResultSqlContainsSelect() + { + // Arrange + var builder = new SqlKataSearchQueryBuilder(_compiler); + + // Act + var result = builder.BuildSearchQuery(1); // Assert result.Sql.ShouldContain("SELECT"); + result.Sql.ShouldContain("WorkOrderNumber"); } [Fact] - public void BuildSearchQuery_WithSingleFilter_ProducesCorrectStructure() + public void BuildSearchQuery_ContainsStepFlaggingQuery() { // Arrange - var workOrderHandler = new WorkOrderFilterHandler(); - var handlers = new IFilterHandler[] { workOrderHandler }; - var builder = new SqlKataSearchQueryBuilder(_compiler, handlers); - var model = new SearchModel - { - WorkOrderFilter = - [ - new WorkOrderFilterEntry { WorkOrderNumber = 12345 } - ] - }; + var builder = new SqlKataSearchQueryBuilder(_compiler); // Act - var result = builder.BuildSearchQuery(model); + var result = builder.BuildSearchQuery(1); // Assert - result.ShouldNotBeNull(); - result.TempTableSetupSql.ShouldNotBeEmpty(); - result.Parameters.ShouldContainKey("p_WorkOrderFilter"); - - var setupSql = string.Join("\n", result.TempTableSetupSql); - // Should have temp table creation and work order merge - setupSql.ShouldContain("#Temp_WO"); - setupSql.ShouldContain("MERGE"); - } - - [Fact] - public void BuildSearchQuery_WithMultipleFilters_CombinesCorrectly() - { - // Arrange - var workOrderHandler = new WorkOrderFilterHandler(); - var itemNumberHandler = new ItemNumberFilterHandler(); - var handlers = new IFilterHandler[] { workOrderHandler, itemNumberHandler }; - var builder = new SqlKataSearchQueryBuilder(_compiler, handlers); - var model = new SearchModel - { - WorkOrderFilter = - [ - new WorkOrderFilterEntry { WorkOrderNumber = 12345 } - ], - ItemNumberFilter = - [ - new ItemNumberFilterEntry { ItemNumber = "ABC123" } - ] - }; - - // Act - var result = builder.BuildSearchQuery(model); - - // Assert - result.ShouldNotBeNull(); - result.Parameters.ShouldContainKey("p_WorkOrderFilter"); - result.Parameters.ShouldContainKey("p_ItemNumberFilter"); - - var setupSql = string.Join("\n", result.TempTableSetupSql); - setupSql.ShouldContain("#Temp_WO"); - setupSql.ShouldContain("#P_ItemNumbers"); - } - - [Fact] - public void BuildSearchQuery_HandlersAreAppliedInPriorityOrder() - { - // Arrange - var lowPriorityHandler = Substitute.For(); - lowPriorityHandler.Priority.Returns(100); - lowPriorityHandler.IsEnabled(Arg.Any()).Returns(true); - lowPriorityHandler.Apply(Arg.Any(), Arg.Any()) - .Returns(new FilterResult(["-- LOW PRIORITY SQL"], new Dictionary())); - - var highPriorityHandler = Substitute.For(); - highPriorityHandler.Priority.Returns(1); - highPriorityHandler.IsEnabled(Arg.Any()).Returns(true); - highPriorityHandler.Apply(Arg.Any(), Arg.Any()) - .Returns(new FilterResult(["-- HIGH PRIORITY SQL"], new Dictionary())); - - // Pass handlers in reverse priority order to verify sorting - var handlers = new[] { lowPriorityHandler, highPriorityHandler }; - var builder = new SqlKataSearchQueryBuilder(_compiler, handlers); - var model = new SearchModel(); - - // Act - var result = builder.BuildSearchQuery(model); - - // Assert - var setupSql = string.Join("\n", result.TempTableSetupSql); - var highIndex = setupSql.IndexOf("-- HIGH PRIORITY SQL", StringComparison.Ordinal); - var lowIndex = setupSql.IndexOf("-- LOW PRIORITY SQL", StringComparison.Ordinal); - - highIndex.ShouldBeGreaterThan(-1); - lowIndex.ShouldBeGreaterThan(-1); - highIndex.ShouldBeLessThan(lowIndex); - } - - [Fact] - public void BuildSearchQuery_DisabledHandlersAreSkipped() - { - // Arrange - var enabledHandler = Substitute.For(); - enabledHandler.Priority.Returns(1); - enabledHandler.IsEnabled(Arg.Any()).Returns(true); - enabledHandler.Apply(Arg.Any(), Arg.Any()) - .Returns(new FilterResult(["-- ENABLED"], new Dictionary())); - - var disabledHandler = Substitute.For(); - disabledHandler.Priority.Returns(2); - disabledHandler.IsEnabled(Arg.Any()).Returns(false); - - var handlers = new[] { enabledHandler, disabledHandler }; - var builder = new SqlKataSearchQueryBuilder(_compiler, handlers); - var model = new SearchModel(); - - // Act - var result = builder.BuildSearchQuery(model); - - // Assert - var setupSql = string.Join("\n", result.TempTableSetupSql); - setupSql.ShouldContain("-- ENABLED"); - - // Apply should never be called on disabled handler - disabledHandler.DidNotReceive().Apply(Arg.Any(), Arg.Any()); - } - - [Fact] - public void BuildSearchQuery_WithTimespanFilter_IncludesStepFlagging() - { - // Arrange - var handlers = Array.Empty(); - var builder = new SqlKataSearchQueryBuilder(_compiler, handlers); - var model = new SearchModel - { - MinimumDt = DateTime.Now.AddDays(-30), - MaximumDt = DateTime.Now - }; - - // Act - var result = builder.BuildSearchQuery(model); - - // Assert - // When ShouldSearchSteps returns true, step flagging query is added var setupSql = string.Join("\n", result.TempTableSetupSql); setupSql.ShouldContain("LU_WO"); setupSql.ShouldContain("Flagged"); + setupSql.ShouldContain("MERGE"); } [Fact] public void BuildMisQuery_ReturnsValidResult() { // Arrange - var handlers = Array.Empty(); - var builder = new SqlKataSearchQueryBuilder(_compiler, handlers); - var model = new SearchModel(); + var builder = new SqlKataSearchQueryBuilder(_compiler); // Act - var result = builder.BuildMisQuery(model); + var result = builder.BuildMisQuery(1); // Assert result.ShouldNotBeNull(); result.Sql.ShouldNotBeNullOrEmpty(); result.Sql.ShouldContain("#TempMisData"); + result.Parameters.ShouldContainKey("SearchId"); } [Fact] public void BuildMisNonMatchQuery_ReturnsValidResult() { // Arrange - var handlers = Array.Empty(); - var builder = new SqlKataSearchQueryBuilder(_compiler, handlers); - var model = new SearchModel(); + var builder = new SqlKataSearchQueryBuilder(_compiler); // Act - var result = builder.BuildMisNonMatchQuery(model); + var result = builder.BuildMisNonMatchQuery(1); // Assert result.ShouldNotBeNull(); result.Sql.ShouldNotBeNullOrEmpty(); result.Sql.ShouldContain("WasJobStepAdded"); result.Sql.ShouldContain("MatchedJobStepNumber"); + result.Parameters.ShouldContainKey("SearchId"); + } + + [Fact] + public void BuildSearchQuery_DifferentSearchIds_ProduceDifferentParameters() + { + // Arrange + var builder = new SqlKataSearchQueryBuilder(_compiler); + + // Act + var result1 = builder.BuildSearchQuery(100); + var result2 = builder.BuildSearchQuery(200); + + // Assert + result1.Parameters["SearchId"].ShouldBe(100); + result2.Parameters["SearchId"].ShouldBe(200); } }