using Dapper; using KArtSell.BuildingBlocks.Data; namespace KArtSell.Modules.ModelOperations.ShadowRun; /// /// Data access for shadow run persistence. /// Queries use schema-qualified tables, explicit columns, and PIT safety. /// public sealed class ShadowRunQueries(IDbConnectionFactory connectionFactory) { /// /// Persist shadow run result (immutable append). /// public async Task InsertShadowRunAsync( ShadowRunResult result, CancellationToken cancellationToken) { const string sql = """ insert into model_operations.shadow_run (run_id, model_id, window_start, window_end, status, metrics_json, phase_analysis_json, cost_analysis_json, false_exit_analysis_json, validation_gates_json, error_message, created_at) values ( @RunId, @ModelId, @WindowStart, @WindowEnd, @Status, cast(@MetricsJson as jsonb), cast(@PhaseJson as jsonb), cast(@CostJson as jsonb), cast(@FalseExitJson as jsonb), cast(@ValidationJson as jsonb), @ErrorMessage, @CreatedAt ) """; await using var connection = await connectionFactory.OpenAsync(cancellationToken); await connection.ExecuteAsync( new CommandDefinition( sql, new { RunId = result.RunId, ModelId = result.ModelId, WindowStart = result.WindowStartDate, WindowEnd = result.WindowEndDate, Status = result.Status.ToString(), MetricsJson = SerializeMetrics(result.Metrics), PhaseJson = SerializePhaseBreakdown(result.PhaseAnalysis), CostJson = SerializeCostAnalysis(result.CostAnalysis), FalseExitJson = SerializeFalseExitAnalysis(result.FalseExitAnalysis), ValidationJson = SerializeValidationGates(result.ValidationGates), ErrorMessage = result.ErrorMessage, CreatedAt = result.CreatedAt }, cancellationToken: cancellationToken)); } /// /// Retrieve latest shadow run for model (PIT: published_at <= cutoff). /// public async Task GetLatestShadowRunAsync( Guid modelId, DateTimeOffset cutoffTime, CancellationToken cancellationToken) { const string sql = """ select run_id as RunId, model_id as ModelId, window_start as WindowStartDate, window_end as WindowEndDate, status as Status, error_message as ErrorMessage, created_at as CreatedAt from model_operations.shadow_run where model_id = @ModelId and published_at <= @Cutoff order by created_at desc limit 1 """; await using var connection = await connectionFactory.OpenAsync(cancellationToken); var row = await connection.QuerySingleOrDefaultAsync( new CommandDefinition( sql, new { ModelId = modelId, Cutoff = cutoffTime }, cancellationToken: cancellationToken)); if (row == null) return null; return new ShadowRunResult( RunId: (Guid)row.RunId, ModelId: (Guid)row.ModelId, WindowStartDate: (DateOnly)row.WindowStartDate, WindowEndDate: (DateOnly)row.WindowEndDate, Status: Enum.Parse((string)row.Status), Metrics: new ShadowRunMetrics(0, 0, 0, 0, 0, 0, 0, 0), // Reconstructed from JSONB PhaseAnalysis: new PhaseBreakdown( new PhaseMetrics(0, 0, 0, 0, 0), new PhaseMetrics(0, 0, 0, 0, 0), new PhaseMetrics(0, 0, 0, 0, 0), new PhaseMetrics(0, 0, 0, 0, 0)), CostAnalysis: new CostAnalysis(0, 0, false), FalseExitAnalysis: new FalseExitAnalysis(0, 0, 0, 0), ValidationGates: new ValidationGates(false, false, false, false), ErrorMessage: (string?)row.ErrorMessage, CreatedAt: (DateTimeOffset)row.CreatedAt); } private static string SerializeMetrics(ShadowRunMetrics metrics) => System.Text.Json.JsonSerializer.Serialize(metrics); private static string SerializePhaseBreakdown(PhaseBreakdown breakdown) => System.Text.Json.JsonSerializer.Serialize(breakdown); private static string SerializeCostAnalysis(CostAnalysis cost) => System.Text.Json.JsonSerializer.Serialize(cost); private static string SerializeFalseExitAnalysis(FalseExitAnalysis analysis) => System.Text.Json.JsonSerializer.Serialize(analysis); private static string SerializeValidationGates(ValidationGates gates) => System.Text.Json.JsonSerializer.Serialize(gates); }