using System.Globalization; using System.Text.Json; using Dapper; using Microsoft.Data.SqlClient; using SQLInsight.Core.Data; namespace SQLInsight.Repository; /// A long operation as stored; is sensitive (local UI only). public sealed record StoredLongOperation(int SessionId, DateTime StartedAt, string Command, string? DatabaseName, string? LoginName, double PercentComplete, DateTime? EstimatedEnd, DateTime LastSeenAt) { /// Still running at the latest sample (seen within the last two minutes). public bool IsRunning(DateTime utcNow) => utcNow - LastSeenAt < TimeSpan.FromMinutes(2); public TimeSpan Duration => LastSeenAt - StartedAt; } public sealed partial class InsightRepository { private static async Task SaveLongOperationsAsync(SqlConnection connection, int instanceId, LongOperationSnapshot snapshot) { if (snapshot.Running.Count == 0) { return; } await connection.ExecuteAsync(""" MERGE insight.LongOperations AS t USING (SELECT * FROM OPENJSON(@json) WITH (SessionId int, StartedAt datetime2(0), Command nvarchar(32), DatabaseName nvarchar(128), LoginName nvarchar(128), PercentComplete float, EstimatedEnd datetime2(0), LastSeenAt datetime2(0))) AS s ON t.InstanceId = @instanceId AND t.StartedAt = s.StartedAt AND t.SessionId = s.SessionId AND t.Command = s.Command WHEN MATCHED THEN UPDATE SET PercentComplete = s.PercentComplete, EstimatedEnd = s.EstimatedEnd, LastSeenAt = s.LastSeenAt WHEN NOT MATCHED THEN INSERT (InstanceId, SessionId, StartedAt, Command, DatabaseName, LoginName, PercentComplete, EstimatedEnd, LastSeenAt) VALUES (@instanceId, s.SessionId, s.StartedAt, s.Command, s.DatabaseName, s.LoginName, s.PercentComplete, s.EstimatedEnd, s.LastSeenAt); """, new { instanceId, json = JsonSerializer.Serialize(snapshot.Running.Select(o => new { o.SessionId, StartedAt = Iso(o.StartedAt), Command = o.Command.Length <= 32 ? o.Command : o.Command[..32], o.DatabaseName, o.LoginName, o.PercentComplete, EstimatedEnd = o.EstimatedEnd is { } end ? Iso(end) : null, LastSeenAt = Iso(o.LastSeenAt), }), Json), }).ConfigureAwait(false); static string Iso(DateTimeOffset value) => Utc(value).ToString("yyyy-MM-ddTHH:mm:ss", CultureInfo.InvariantCulture); } /// Long operations that ran at some point in [from, to], newest first. public async Task> GetLongOperationsAsync(int instanceId, DateTimeOffset from, DateTimeOffset to, int top = 200, CancellationToken cancellationToken = default) { await using var connection = await OpenAsync(cancellationToken).ConfigureAwait(false); var rows = await connection.QueryAsync(""" SELECT TOP (@top) SessionId, StartedAt, Command, DatabaseName, LoginName, PercentComplete, EstimatedEnd, LastSeenAt FROM insight.LongOperations WHERE InstanceId = @instanceId AND StartedAt <= @to AND LastSeenAt >= @from ORDER BY StartedAt DESC; """, new { instanceId, from = from.UtcDateTime, to = to.UtcDateTime, top }).ConfigureAwait(false); return rows.ToList(); } }