using ScadaLink.Commons.Entities.Audit; using ScadaLink.Commons.Types.Audit; namespace ScadaLink.Commons.Interfaces.Repositories; /// /// Append-only data access for the central AuditLog table (Audit Log #23). /// /// /// /// The append-only invariant is enforced both at the SQL level (the /// scadalink_audit_writer role has only INSERT + SELECT — UPDATE and DELETE /// are not granted) and at the API level: this interface deliberately exposes no /// Update and no single-row Delete. Bulk purge is performed exclusively via /// monthly partition switch-out (). /// /// /// Ingest is idempotent on EventId: is /// first-write-wins, so retrying telemetry and reconciliation pulls can both feed /// the same writer without producing duplicates. /// /// public interface IAuditLogRepository { /// /// Inserts if no row with the same /// exists; otherwise silently leaves the /// stored row untouched (first-write-wins). Bypasses the EF change tracker /// so the row never enters a tracked state. /// Task InsertIfNotExistsAsync(AuditEvent evt, CancellationToken ct = default); /// /// Returns up to rows matching /// , ordered by (OccurredAtUtc DESC, EventId DESC). /// Use keyset paging by passing the last returned row's /// OccurredAtUtc + EventId back via /// + /// to fetch the next page. /// Task> QueryAsync( AuditLogQueryFilter filter, AuditLogPaging paging, CancellationToken ct = default); /// /// Switches out (purges) the monthly partition whose lower bound is /// . /// /// /// /// Drop-and-rebuild dance. UX_AuditLog_EventId is intentionally /// non-partition-aligned (it lives on [PRIMARY] so single-column /// EventId uniqueness — required by — /// can be enforced cheaply). SQL Server rejects /// ALTER TABLE … SWITCH PARTITION while a non-aligned unique index /// is present, so the M6 implementation drops the index, creates a staging /// table with byte-identical schema, switches the partition's data into /// staging, drops staging (discarding the rows), and rebuilds the unique /// index. The CATCH branch guarantees the index is rebuilt even on partial /// failure so the table never returns to live traffic without its /// idempotency-supporting index. /// /// /// Outage window. The dance briefly removes the unique index, so /// concurrent calls during the switch /// could in principle race past the IF NOT EXISTS check without the index /// catching the duplicate. This is acceptable for the daily purge cadence /// — the inserts that the IF NOT EXISTS check guards are themselves rare /// enough that a sub-second collision window is operationally negligible, /// and the composite PK still rejects same-(EventId, OccurredAtUtc) rows. /// /// Task SwitchOutPartitionAsync(DateTime monthBoundary, CancellationToken ct = default); /// /// Returns the set of pf_AuditLog_Month partition lower-bound /// boundaries whose partitions contain only rows with /// strictly older than /// . Boundaries whose partition is empty are /// excluded (a no-op switch is wasted work). Used by the M6 purge actor /// to enumerate retention-eligible months on every tick. /// Task> GetPartitionBoundariesOlderThanAsync( DateTime threshold, CancellationToken ct = default); }