using System.Collections.Generic; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using GB5Shared.GOP.GBQueryExecutor; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.Qualifier; using Microsoft.Extensions.Logging; namespace GB5Shared.GOP.Qualifier.Executors { // ============================================================ // DataSourceExecutor — ExecutionMode 2 (GBQueryExecutor) // // Executes a configured SQL query via IGBQueryExecutor and // writes the returned row's columns into the FactBag as // document-level facts. // // The query is identified by SnapshotMethod.QueryCode. // Parameters are bound from two sources, matching NCalcEvaluator's // precedence: ctx.Document's root properties (the caller-supplied // input payload, e.g. QUALIFY step's InputKeys) first, then FactBag // document facts on top (so downstream queries can override with // outputs already produced by earlier qualifiers in the same run). // // No row returned = no facts written (not an error unless a // rule condition checks for the fact's presence). // ============================================================ public class DataSourceExecutor : IQualifierMethodExecutor { private readonly IGBQueryExecutor _GBQueryExecutor; private readonly ILogger _Logger; public DataSourceExecutor( IGBQueryExecutor gbQueryExecutor, ILogger logger) { _GBQueryExecutor = gbQueryExecutor; _Logger = logger; } public string ExecutionModeKey => "DataSource"; public async Task> ExecuteAsync( SnapshotMethod method, QualifierExecutionContext ctx, LoginDTO loginDTO, CancellationToken ct) { if (string.IsNullOrWhiteSpace(method.QueryCode)) return []; var parameters = new Dictionary(); // Bind the caller-supplied input document first (e.g. QUALIFY step's // InputKeys payload) — same source NCalcEvaluator reads. if (ctx.Document is not null) { try { foreach (var prop in ctx.Document.RootElement.EnumerateObject()) parameters[prop.Name] = ExtractJsonValue(prop.Value); } catch { // Non-object root — no document params to bind. } } // FactBag document facts take precedence — outputs already produced // by earlier qualifiers in this run override the caller's input. var exported = ctx.Facts.ExportAll(); if (exported.TryGetValue("Document", out var docSection) && docSection is IReadOnlyDictionary docFacts) { foreach (var (k, v) in docFacts) parameters[k] = v; } // Also pass EntityId so queries can filter by the specific entity. parameters["EntityId"] = ctx.EntityId; var result = await _GBQueryExecutor.ExecuteRowAsync( method.QueryCode, parameters, loginDTO, callerModule: "QualifierEngine/DataSourceExecutor", ct: ct); if (result.Found) { // Write returned columns as document-level facts. foreach (var (col, val) in result.Row) ctx.Facts.SetDocumentFact(col, val); _Logger.LogDebug( "DataSourceExecutor: query '{QueryCode}' returned {ColCount} columns for entity {EntityId}", method.QueryCode, result.Row.Count, ctx.EntityId); } else { _Logger.LogDebug( "DataSourceExecutor: query '{QueryCode}' returned no row for entity {EntityId}", method.QueryCode, ctx.EntityId); } // DataSource methods populate facts; findings come from rule conditions. return []; } private static object? ExtractJsonValue(JsonElement element) => element.ValueKind switch { JsonValueKind.String => element.GetString(), JsonValueKind.Number => element.TryGetInt64(out var l) ? (object)l : element.GetDouble(), JsonValueKind.True => true, JsonValueKind.False => false, JsonValueKind.Null => null, _ => element.GetRawText() }; } }