218 lines
8.5 KiB
C#
218 lines
8.5 KiB
C#
using System.Runtime.CompilerServices;
|
|
using Dapper;
|
|
using JdeScoping.Core.Interfaces;
|
|
using JdeScoping.DataAccess.Options;
|
|
using JdeScoping.DataAccess.Interfaces;
|
|
using JdeScoping.Core.Models.SearchResults;
|
|
using JdeScoping.DataAccess.Models;
|
|
using JdeScoping.DataAccess.QueryBuilders;
|
|
using Microsoft.Data.SqlClient;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using SqlKata.Compilers;
|
|
|
|
namespace JdeScoping.DataAccess.Services;
|
|
|
|
/// <summary>
|
|
/// Main search processor service that orchestrates search execution.
|
|
/// </summary>
|
|
public sealed class SearchProcessor : ISearchProcessor
|
|
{
|
|
private readonly IDbConnectionFactory _connectionFactory;
|
|
private readonly ISearchQueryBuilder _queryBuilder;
|
|
private readonly IWorkOrderTraversalService _traversalService;
|
|
private readonly MisQueryBuilder _misQueryBuilder;
|
|
private readonly SearchProcessingConfiguration _options;
|
|
private readonly ILogger<SearchProcessor> _logger;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of SearchProcessor.
|
|
/// </summary>
|
|
public SearchProcessor(
|
|
IDbConnectionFactory connectionFactory,
|
|
ISearchQueryBuilder queryBuilder,
|
|
IWorkOrderTraversalService traversalService,
|
|
SqlServerCompiler compiler,
|
|
IOptions<SearchProcessingConfiguration> options,
|
|
ILogger<SearchProcessor> logger)
|
|
{
|
|
_connectionFactory = connectionFactory;
|
|
_queryBuilder = queryBuilder;
|
|
_traversalService = traversalService;
|
|
_misQueryBuilder = new MisQueryBuilder(compiler);
|
|
_options = options.Value;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Executes search and returns results as async stream.
|
|
/// </summary>
|
|
/// <param name="model">The search model containing filter criteria.</param>
|
|
/// <param name="ct">Cancellation token.</param>
|
|
/// <returns>Async enumerable of search results.</returns>
|
|
public async IAsyncEnumerable<SearchResult> ExecuteSearchAsync(
|
|
SearchModel model,
|
|
[EnumeratorCancellation] CancellationToken ct = default)
|
|
{
|
|
_logger.LogInformation("Executing search {SearchId}", model.Id);
|
|
|
|
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
|
|
|
|
// Build the search query using searchId only
|
|
var queryResult = _queryBuilder.BuildSearchQuery(model.Id);
|
|
|
|
if (_options.EnableDebugSql && !string.IsNullOrEmpty(_options.DebugSqlPath))
|
|
{
|
|
await WriteDebugSqlAsync(model.Id, queryResult);
|
|
}
|
|
|
|
// Execute setup SQL (temp tables, filter population)
|
|
foreach (var setupSql in queryResult.TempTableSetupSql)
|
|
{
|
|
_logger.LogDebug("Executing setup SQL: {Sql}", setupSql[..Math.Min(100, setupSql.Length)]);
|
|
await connection.ExecuteAsync(
|
|
setupSql,
|
|
queryResult.Parameters,
|
|
commandTimeout: _options.QueryTimeoutSeconds);
|
|
}
|
|
|
|
// Execute downstream traversal
|
|
await _traversalService.TraverseDownstreamAsync(
|
|
connection,
|
|
_options.MaxTraversalIterations,
|
|
ct);
|
|
|
|
// Stream results using unbuffered query
|
|
_logger.LogDebug("Executing result query");
|
|
var reader = await connection.QueryAsync<SearchResult>(
|
|
queryResult.Sql,
|
|
queryResult.Parameters,
|
|
commandTimeout: _options.QueryTimeoutSeconds);
|
|
|
|
foreach (var result in reader)
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
yield return result;
|
|
}
|
|
|
|
_logger.LogInformation("Search {SearchId} completed", model.Id);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Executes search and materializes all results into SearchModel.
|
|
/// </summary>
|
|
/// <param name="model">The search model containing filter criteria.</param>
|
|
/// <param name="ct">Cancellation token.</param>
|
|
/// <returns>The SearchModel populated with results.</returns>
|
|
public async Task<SearchModel> ExecuteSearchToModelAsync(
|
|
SearchModel model,
|
|
CancellationToken ct = default)
|
|
{
|
|
_logger.LogInformation("Executing search {SearchId} to model", model.Id);
|
|
|
|
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
|
|
|
|
// Build the search query using searchId only
|
|
var queryResult = _queryBuilder.BuildSearchQuery(model.Id);
|
|
|
|
if (_options.EnableDebugSql && !string.IsNullOrEmpty(_options.DebugSqlPath))
|
|
{
|
|
await WriteDebugSqlAsync(model.Id, queryResult);
|
|
}
|
|
|
|
// Execute setup SQL (temp tables, filter population)
|
|
foreach (var setupSql in queryResult.TempTableSetupSql)
|
|
{
|
|
_logger.LogDebug("Executing setup SQL: {Sql}", setupSql[..Math.Min(100, setupSql.Length)]);
|
|
await connection.ExecuteAsync(
|
|
setupSql,
|
|
queryResult.Parameters,
|
|
commandTimeout: _options.QueryTimeoutSeconds);
|
|
}
|
|
|
|
// Execute downstream traversal
|
|
await _traversalService.TraverseDownstreamAsync(
|
|
connection,
|
|
_options.MaxTraversalIterations,
|
|
ct);
|
|
|
|
// Execute result query and materialize
|
|
_logger.LogDebug("Executing result query");
|
|
var results = await connection.QueryAsync<SearchResult>(
|
|
queryResult.Sql,
|
|
queryResult.Parameters,
|
|
commandTimeout: _options.QueryTimeoutSeconds);
|
|
|
|
model.Results = results.ToList();
|
|
_logger.LogInformation("Search {SearchId} returned {ResultCount} results", model.Id, model.Results.Count);
|
|
|
|
// Extract MIS data if requested (check ExtractMisData from database using extraction function)
|
|
var extractMisData = await connection.QuerySingleOrDefaultAsync<bool?>(
|
|
"SELECT dbo.fn_GetSearchExtractMisData(@SearchId)",
|
|
new { SearchId = model.Id },
|
|
commandTimeout: _options.QueryTimeoutSeconds) ?? false;
|
|
|
|
if (extractMisData)
|
|
{
|
|
await ExecuteMisExtractionAsync(model, connection, ct);
|
|
}
|
|
|
|
return model;
|
|
}
|
|
|
|
private async Task ExecuteMisExtractionAsync(
|
|
SearchModel model,
|
|
SqlConnection connection,
|
|
CancellationToken ct)
|
|
{
|
|
_logger.LogDebug("Extracting MIS data for search {SearchId}", model.Id);
|
|
|
|
// Build and execute MIS setup SQL (uses temp tables and variables from main query)
|
|
var misSetupStatements = _misQueryBuilder.BuildMisExtractionSql(model.Id);
|
|
|
|
foreach (var sql in misSetupStatements)
|
|
{
|
|
await connection.ExecuteAsync(
|
|
sql,
|
|
new { SearchId = model.Id },
|
|
commandTimeout: _options.QueryTimeoutSeconds);
|
|
}
|
|
|
|
// Execute MIS result query
|
|
var misQueryResult = _queryBuilder.BuildMisQuery(model.Id);
|
|
var misResults = await connection.QueryAsync<MisSearchResult>(
|
|
misQueryResult.Sql,
|
|
misQueryResult.Parameters,
|
|
commandTimeout: _options.QueryTimeoutSeconds);
|
|
|
|
model.MisResults = misResults.ToList();
|
|
_logger.LogDebug("Found {MisResultCount} MIS results", model.MisResults.Count);
|
|
|
|
// Execute MIS non-match query
|
|
var misNonMatchQueryResult = _queryBuilder.BuildMisNonMatchQuery(model.Id);
|
|
var misNonMatchResults = await connection.QueryAsync<MisNonMatchSearchResult>(
|
|
misNonMatchQueryResult.Sql,
|
|
misNonMatchQueryResult.Parameters,
|
|
commandTimeout: _options.QueryTimeoutSeconds);
|
|
|
|
model.MisNonMatchResults = misNonMatchResults.ToList();
|
|
_logger.LogDebug("Found {MisNonMatchCount} MIS non-match results", model.MisNonMatchResults.Count);
|
|
}
|
|
|
|
private async Task WriteDebugSqlAsync(int searchId, SearchQueryResult queryResult)
|
|
{
|
|
try
|
|
{
|
|
var debugPath = Path.Combine(_options.DebugSqlPath!, $"search_{searchId}_{DateTime.UtcNow:yyyyMMdd_HHmmss}.sql");
|
|
var content = string.Join("\r\n\r\n-- ========================================\r\n\r\n",
|
|
queryResult.TempTableSetupSql.Append(queryResult.Sql));
|
|
await File.WriteAllTextAsync(debugPath, content);
|
|
_logger.LogDebug("Debug SQL written to {Path}", debugPath);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Failed to write debug SQL");
|
|
}
|
|
}
|
|
}
|