using Dapper; using Microsoft.Extensions.Logging; using Quartz; using SIGCM2.Infrastructure.Persistence; namespace SIGCM2.Infrastructure.Audit.Jobs; /// /// UDT-010 (#REQ-AUD-6) — monthly maintenance: /// - Extends the forward boundary of pf_AuditEvent_Monthly and pf_SecurityEvent_Monthly /// so the next month always has a partition ready (SPLIT RANGE). /// - Intended schedule: cron '0 0 2 1 * ?' (day 1 each month at 02:00 UTC). /// - Idempotent: only splits if the target boundary does not yet exist. /// [DisallowConcurrentExecution] public sealed class AuditPartitionManagerJob : IJob { public const string CronSchedule = "0 0 2 1 * ?"; private readonly SqlConnectionFactory _factory; private readonly ILogger _logger; private readonly TimeProvider _timeProvider; public AuditPartitionManagerJob(SqlConnectionFactory factory, ILogger logger, TimeProvider timeProvider) { _factory = factory; _logger = logger; _timeProvider = timeProvider; } public async Task Execute(IJobExecutionContext context) { var ct = context.CancellationToken; await using var conn = _factory.CreateConnection(); await conn.OpenAsync(ct); // Target: boundary for "next month + 1" (so the next month is always pre-created and we // keep at least one boundary ahead after the rotation). var now = _timeProvider.GetUtcNow().UtcDateTime; var target = new DateTime(now.Year, now.Month, 1, 0, 0, 0, DateTimeKind.Utc).AddMonths(2); var affected = 0; foreach (var pf in new[] { "pf_AuditEvent_Monthly", "pf_SecurityEvent_Monthly" }) { var exists = await conn.ExecuteScalarAsync(""" SELECT COUNT(*) FROM sys.partition_functions pf JOIN sys.partition_range_values prv ON prv.function_id = pf.function_id WHERE pf.name = @Name AND CAST(prv.value AS DATETIME2(3)) = @Target; """, new { Name = pf, Target = target }); if (exists == 0) { // Parameterized partition function name would require dynamic SQL; whitelisted above. var sql = $"ALTER PARTITION FUNCTION {pf}() SPLIT RANGE (@Target);"; await conn.ExecuteAsync(sql, new { Target = target }); affected++; _logger.LogInformation("Partition boundary {Boundary:yyyy-MM-dd} added to {Function}", target, pf); } } _logger.LogInformation( "AuditPartitionManagerJob completed — {Affected} partition function(s) extended (target: {Target:yyyy-MM-dd})", affected, target); } }