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
This commit is contained in:
@@ -3,28 +3,29 @@ using JdeScoping.DataAccess.Models;
|
||||
namespace JdeScoping.DataAccess.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for building search queries using SqlKata.
|
||||
/// Builds SQL queries for search operations.
|
||||
/// Uses SQL extraction functions to retrieve criteria from the Search table.
|
||||
/// </summary>
|
||||
public interface ISearchQueryBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds the main search query for flagging and retrieving work orders.
|
||||
/// Builds the main search query using extraction functions.
|
||||
/// </summary>
|
||||
/// <param name="model">The search model containing filter criteria.</param>
|
||||
/// <returns>The compiled query result with SQL, parameters, and setup statements.</returns>
|
||||
SearchQueryResult BuildSearchQuery(SearchModel model);
|
||||
/// <param name="searchId">The search ID to extract criteria from.</param>
|
||||
/// <returns>Query result with SQL and parameters.</returns>
|
||||
SearchQueryResult BuildSearchQuery(int searchId);
|
||||
|
||||
/// <summary>
|
||||
/// Builds the MIS data extraction query when ExtractMisData is enabled.
|
||||
/// Builds the MIS data extraction query.
|
||||
/// </summary>
|
||||
/// <param name="model">The search model containing filter criteria.</param>
|
||||
/// <returns>The compiled query result for MIS extraction.</returns>
|
||||
SearchQueryResult BuildMisQuery(SearchModel model);
|
||||
/// <param name="searchId">The search ID to extract criteria from.</param>
|
||||
/// <returns>Query result with SQL and parameters.</returns>
|
||||
SearchQueryResult BuildMisQuery(int searchId);
|
||||
|
||||
/// <summary>
|
||||
/// Builds the MIS non-match query for work orders without MIS records.
|
||||
/// Builds the MIS non-match extraction query.
|
||||
/// </summary>
|
||||
/// <param name="model">The search model containing filter criteria.</param>
|
||||
/// <returns>The compiled query result for MIS non-match extraction.</returns>
|
||||
SearchQueryResult BuildMisNonMatchQuery(SearchModel model);
|
||||
/// <param name="searchId">The search ID.</param>
|
||||
/// <returns>Query result with SQL and parameters.</returns>
|
||||
SearchQueryResult BuildMisNonMatchQuery(int searchId);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public sealed class SqlKataSearchQueryBuilder : ISearchQueryBuilder
|
||||
{
|
||||
private readonly SqlServerCompiler _compiler;
|
||||
private readonly IEnumerable<IFilterHandler> _filterHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of SqlKataSearchQueryBuilder.
|
||||
/// </summary>
|
||||
/// <param name="compiler">The SqlKata SQL Server compiler.</param>
|
||||
/// <param name="filterHandlers">Collection of filter handlers.</param>
|
||||
public SqlKataSearchQueryBuilder(
|
||||
SqlServerCompiler compiler,
|
||||
IEnumerable<IFilterHandler> filterHandlers)
|
||||
public SqlKataSearchQueryBuilder(SqlServerCompiler compiler)
|
||||
{
|
||||
_compiler = compiler;
|
||||
_filterHandlers = filterHandlers.OrderBy(h => h.Priority);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public SearchQueryResult BuildSearchQuery(SearchModel model)
|
||||
public SearchQueryResult BuildSearchQuery(int searchId)
|
||||
{
|
||||
var setupStatements = new List<string>();
|
||||
var parameters = new Dictionary<string, object>();
|
||||
var parameters = new Dictionary<string, object>
|
||||
{
|
||||
["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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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<string, object>();
|
||||
|
||||
if (model.MinimumDt.HasValue)
|
||||
var parameters = new Dictionary<string, object>
|
||||
{
|
||||
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, []);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public SearchQueryResult BuildMisNonMatchQuery(SearchModel model)
|
||||
public SearchQueryResult BuildMisNonMatchQuery(int searchId)
|
||||
{
|
||||
var parameters = new Dictionary<string, object>
|
||||
{
|
||||
["SearchId"] = searchId
|
||||
};
|
||||
|
||||
var sql = BuildMisNonMatchExtractionQuery();
|
||||
return new SearchQueryResult(sql, new Dictionary<string, object>(), []);
|
||||
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<string>();
|
||||
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<string>();
|
||||
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<string>();
|
||||
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
|
||||
|
||||
@@ -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<MisSearchResult>(
|
||||
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<MisNonMatchSearchResult>(
|
||||
misNonMatchQueryResult.Sql,
|
||||
misNonMatchQueryResult.Parameters,
|
||||
|
||||
+106
-158
@@ -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<IFilterHandler>();
|
||||
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<IFilterHandler>();
|
||||
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<IFilterHandler>();
|
||||
lowPriorityHandler.Priority.Returns(100);
|
||||
lowPriorityHandler.IsEnabled(Arg.Any<SearchModel>()).Returns(true);
|
||||
lowPriorityHandler.Apply(Arg.Any<SearchModel>(), Arg.Any<SqlServerCompiler>())
|
||||
.Returns(new FilterResult(["-- LOW PRIORITY SQL"], new Dictionary<string, object>()));
|
||||
|
||||
var highPriorityHandler = Substitute.For<IFilterHandler>();
|
||||
highPriorityHandler.Priority.Returns(1);
|
||||
highPriorityHandler.IsEnabled(Arg.Any<SearchModel>()).Returns(true);
|
||||
highPriorityHandler.Apply(Arg.Any<SearchModel>(), Arg.Any<SqlServerCompiler>())
|
||||
.Returns(new FilterResult(["-- HIGH PRIORITY SQL"], new Dictionary<string, object>()));
|
||||
|
||||
// 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<IFilterHandler>();
|
||||
enabledHandler.Priority.Returns(1);
|
||||
enabledHandler.IsEnabled(Arg.Any<SearchModel>()).Returns(true);
|
||||
enabledHandler.Apply(Arg.Any<SearchModel>(), Arg.Any<SqlServerCompiler>())
|
||||
.Returns(new FilterResult(["-- ENABLED"], new Dictionary<string, object>()));
|
||||
|
||||
var disabledHandler = Substitute.For<IFilterHandler>();
|
||||
disabledHandler.Priority.Returns(2);
|
||||
disabledHandler.IsEnabled(Arg.Any<SearchModel>()).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<SearchModel>(), Arg.Any<SqlServerCompiler>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildSearchQuery_WithTimespanFilter_IncludesStepFlagging()
|
||||
{
|
||||
// Arrange
|
||||
var handlers = Array.Empty<IFilterHandler>();
|
||||
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<IFilterHandler>();
|
||||
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<IFilterHandler>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user