using System.Text.RegularExpressions; using Microsoft.Data.SqlClient; namespace SwOfflineApply; /// /// Applies a decrypted Offline Rollout Package's scripts against a local SQL Server, and writes /// its own local, append-only execution log (a plain JSON-lines file next to the package) — /// proof of what ran and when, entirely local, with no dependency on reporting back to central. /// GO-splitting logic mirrors SwBLL.Provisioning.TargetDbExecutor.ExecuteScriptAsync's own /// regex — same convention, independently implemented per this tool's zero-GB5-dependency rule. /// public static class LocalScriptApplier { public record ScriptResult(string? ObjectName, bool Success, int RowsAffected, string? Error); public static async Task> ApplyAsync( OfflinePackageBundle bundle, string connectionString, string logFilePath, CancellationToken ct) { var results = new List(); await using var conn = new SqlConnection(connectionString); await conn.OpenAsync(ct).ConfigureAwait(false); foreach (var script in bundle.Scripts.OrderBy(s => s.Sequence)) { var batches = Regex.Split(script.SqlScript, @"^\s*GO\s*$", RegexOptions.Multiline | RegexOptions.IgnoreCase) .Where(b => !string.IsNullOrWhiteSpace(b)) .ToList(); int totalRows = 0; try { foreach (var batch in batches) { await using var cmd = new SqlCommand(batch, conn) { CommandTimeout = 300 }; totalRows += await cmd.ExecuteNonQueryAsync(ct).ConfigureAwait(false); } results.Add(new ScriptResult(script.ObjectName, true, totalRows, null)); } catch (Exception ex) { results.Add(new ScriptResult(script.ObjectName, false, totalRows, ex.Message)); // One script's failure doesn't abort the rest of the batch — matches this // platform's own established posture (ChangeRequestBLL.Execute's per-script // try/catch), so a partial, honestly-reported apply beats an all-or-nothing one. } } await WriteLocalLogAsync(bundle, results, logFilePath, ct).ConfigureAwait(false); return results; } private static async Task WriteLocalLogAsync( OfflinePackageBundle bundle, List results, string logFilePath, CancellationToken ct) { var lines = results.Select(r => $"{{\"AppliedOn\":\"{DateTime.UtcNow:O}\",\"PackageId\":{bundle.PackageId}," + $"\"PackageCode\":\"{bundle.PackageCode}\",\"ObjectName\":\"{r.ObjectName}\"," + $"\"Success\":{r.Success.ToString().ToLowerInvariant()},\"RowsAffected\":{r.RowsAffected}," + $"\"Error\":{(r.Error is null ? "null" : $"\"{r.Error.Replace("\"", "'")}\"")}}}"); await File.AppendAllLinesAsync(logFilePath, lines, ct).ConfigureAwait(false); } }