using ScadaLink.Commons.Entities.Audit; using ScadaLink.Commons.Types; 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. /// /// The audit event to insert. /// Cancellation token. 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. /// /// Filter criteria to apply to the query. /// Paging cursor and page size. /// Cancellation token. Task> QueryAsync( AuditLogQueryFilter filter, AuditLogPaging paging, CancellationToken ct = default); /// /// Switches out (purges) the monthly partition whose lower bound is /// and returns the approximate number /// of rows discarded — sampled inside the transaction BEFORE the switch /// so the row count reflects what the switch removed, not a post-purge /// scan of a table that no longer exists. /// /// /// /// 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. /// /// /// Lower-bound datetime of the monthly partition to switch out. /// Cancellation token. 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. /// /// Only partitions whose data is entirely older than this UTC datetime are returned. /// Cancellation token. Task> GetPartitionBoundariesOlderThanAsync( DateTime threshold, CancellationToken ct = default); /// /// Audit Log (#23) M7 Bundle E (T13) — returns aggregate counts over the /// trailing driving the central Health /// dashboard's Audit KPI tiles. /// /// /// Trailing time window (e.g. TimeSpan.FromHours(1)). Rows whose /// OccurredAtUtc >= nowUtc - window are counted; the upper /// bound is . /// /// /// Optional explicit "now" timestamp used to anchor the trailing window. /// Defaults to at call time when null — /// production callers should leave this null; tests pin a deterministic /// value so the window is reproducible across runs. /// /// Cancellation token. /// /// A snapshot with TotalEventsLastHour + ErrorEventsLastHour /// populated; BacklogTotal is left at zero (this method has no /// visibility into per-site backlogs — the service layer composes it in /// from ). /// AsOfUtc is set to the server-side UtcNow at the time of /// the query. /// /// /// /// Implemented as a single aggregate query /// (SELECT COUNT_BIG(*) AS Total, SUM(CASE …) AS Errors) rather than /// two round trips so the volume + error rate tiles read a consistent /// snapshot — the denominator and numerator come from the same scan. /// /// /// Errors are defined as , /// , or /// /// — every non-success terminal lifecycle state. Submitted, /// Forwarded, Attempted are in-flight and are NOT errors; /// Delivered is success; Skipped is an intentional no-op. /// /// Task GetKpiSnapshotAsync( TimeSpan window, DateTime? nowUtc = null, CancellationToken ct = default); /// /// Audit Log ParentExecutionId feature (Task 8) — given any /// in an execution chain, returns the whole /// chain rooted at the topmost ancestor: one /// per distinct execution, summarising its AuditLog rows. The Central /// UI renders the result as a tree. /// /// /// /// The input id may be any node in the chain — a leaf, the root, or a middle /// node. The implementation first walks up via /// ParentExecutionId to find the root, then walks down from /// the root via a recursive CTE, so the full chain is returned regardless of /// entry point. /// /// /// The ParentExecutionId graph is a tree (acyclic by construction — /// each execution is minted fresh and its parent always pre-exists). Both /// the upward walk and the downward CTE are nonetheless bounded at 32 levels /// as a guard against corrupt/pathological data: a depth that exceeds the /// guard raises an error rather than hanging the server. Chains are shallow /// (1-2 levels typical) so the guard is never reached in practice. /// /// /// A "stub" node — an execution that emitted no rows of its own yet is /// referenced by a child via ParentExecutionId, or whose rows have /// been purged — still appears, with /// = 0. A purged/missing parent simply ends the upward walk. /// /// /// When no AuditLog row carries in /// either ExecutionId or ParentExecutionId, the result is a /// single stub node for itself /// ( = 0) — consistent with the /// stub-node treatment of any other row-less execution. /// /// /// Any execution id in the chain; the implementation walks to the root and back down. /// Cancellation token. Task> GetExecutionTreeAsync( Guid executionId, CancellationToken ct = default); /// /// Returns the distinct, non-null SourceNode values present in the /// AuditLog table, in ascending order. Backs the Audit Log page's /// "Node" multi-select filter dropdown — the Central UI caches the result /// for ~60s so the repository is hit at most once per minute per circuit. /// /// Cancellation token. Task> GetDistinctSourceNodesAsync(CancellationToken ct = default); }