-- V010__audit_infrastructure.sql
-- UDT-010: Infraestructura de Auditoría y Trazabilidad (Fase 0.5 — transversal).
--
-- Cambios:
--   1. Filegroups AUDIT_HOT y AUDIT_COLD (archivos físicos en default data path).
--   2. Partition functions + schemes mensuales (RANGE RIGHT) para AuditEvent y SecurityEvent.
--   3. dbo.AuditEvent particionada + 4 índices + CHECK constraints.
--   4. dbo.SecurityEvent particionada + 3 índices + CHECK constraints.
--   5. SYSTEM_VERSIONING en dbo.Usuario, dbo.Rol, dbo.Permiso, dbo.RolPermiso
--      con HISTORY_RETENTION_PERIOD = 10 YEARS y PAGE compression en history tables.
--
-- Source of truth del diseño: Obsidian/02-ARQUITECTURA-y-TECH-STACK/2.5 📋 Auditoría.md
-- Idempotente: seguro para re-ejecutar.
-- Reversa: V010_ROLLBACK.sql (pierde TODA la historia auditada).
-- Run on: SIGCM2 (dev) y SIGCM2_Test (integration tests). Para producción futuro,
-- revisar database/README.md (ventana de mantenimiento + backup previo).

SET QUOTED_IDENTIFIER ON;
SET ANSI_NULLS ON;
SET NOCOUNT ON;
GO

-- ═══════════════════════════════════════════════════════════════════════
-- 1. FILEGROUPS + ARCHIVOS FÍSICOS
-- ═══════════════════════════════════════════════════════════════════════
-- Usamos el default data path del server + DB_NAME como prefijo lógico,
-- así SIGCM2 y SIGCM2_Test coexisten sin colisión de logical file names.

DECLARE @dbName       NVARCHAR(128) = DB_NAME();
DECLARE @dataPath     NVARCHAR(260) = CAST(SERVERPROPERTY('InstanceDefaultDataPath') AS NVARCHAR(260));
DECLARE @hotLogical   NVARCHAR(128) = @dbName + N'_AUDIT_HOT';
DECLARE @coldLogical  NVARCHAR(128) = @dbName + N'_AUDIT_COLD';
DECLARE @hotPhysical  NVARCHAR(260) = @dataPath + @hotLogical + N'.ndf';
DECLARE @coldPhysical NVARCHAR(260) = @dataPath + @coldLogical + N'.ndf';
DECLARE @sql          NVARCHAR(MAX);

-- Filegroup AUDIT_HOT
IF NOT EXISTS (SELECT 1 FROM sys.filegroups WHERE name = 'AUDIT_HOT')
BEGIN
    EXEC sp_executesql N'ALTER DATABASE CURRENT ADD FILEGROUP AUDIT_HOT;';
    PRINT 'Filegroup AUDIT_HOT created.';
END
ELSE
    PRINT 'Filegroup AUDIT_HOT already exists — skip.';

IF NOT EXISTS (SELECT 1 FROM sys.database_files WHERE name = @hotLogical)
BEGIN
    SET @sql = N'ALTER DATABASE CURRENT ADD FILE (
        NAME = N''' + @hotLogical + N''',
        FILENAME = N''' + @hotPhysical + N''',
        SIZE = 64MB,
        FILEGROWTH = 64MB
    ) TO FILEGROUP AUDIT_HOT;';
    EXEC sp_executesql @sql;
    PRINT 'File ' + @hotLogical + ' added to filegroup AUDIT_HOT.';
END
ELSE
    PRINT 'File ' + @hotLogical + ' already exists — skip.';

-- Filegroup AUDIT_COLD
IF NOT EXISTS (SELECT 1 FROM sys.filegroups WHERE name = 'AUDIT_COLD')
BEGIN
    EXEC sp_executesql N'ALTER DATABASE CURRENT ADD FILEGROUP AUDIT_COLD;';
    PRINT 'Filegroup AUDIT_COLD created.';
END
ELSE
    PRINT 'Filegroup AUDIT_COLD already exists — skip.';

IF NOT EXISTS (SELECT 1 FROM sys.database_files WHERE name = @coldLogical)
BEGIN
    SET @sql = N'ALTER DATABASE CURRENT ADD FILE (
        NAME = N''' + @coldLogical + N''',
        FILENAME = N''' + @coldPhysical + N''',
        SIZE = 64MB,
        FILEGROWTH = 64MB
    ) TO FILEGROUP AUDIT_COLD;';
    EXEC sp_executesql @sql;
    PRINT 'File ' + @coldLogical + ' added to filegroup AUDIT_COLD.';
END
ELSE
    PRINT 'File ' + @coldLogical + ' already exists — skip.';
GO

-- ═══════════════════════════════════════════════════════════════════════
-- 2. PARTITION FUNCTIONS + SCHEMES (mensuales, RANGE RIGHT)
-- ═══════════════════════════════════════════════════════════════════════
-- Boundaries iniciales: 14 valores de 2026-01-01 a 2027-02-01 → 15 particiones.
-- AuditPartitionManagerJob (B11) extiende mes a mes automáticamente.

IF NOT EXISTS (SELECT 1 FROM sys.partition_functions WHERE name = 'pf_AuditEvent_Monthly')
BEGIN
    CREATE PARTITION FUNCTION pf_AuditEvent_Monthly (DATETIME2(3))
    AS RANGE RIGHT FOR VALUES (
        '2026-01-01T00:00:00.000', '2026-02-01T00:00:00.000', '2026-03-01T00:00:00.000',
        '2026-04-01T00:00:00.000', '2026-05-01T00:00:00.000', '2026-06-01T00:00:00.000',
        '2026-07-01T00:00:00.000', '2026-08-01T00:00:00.000', '2026-09-01T00:00:00.000',
        '2026-10-01T00:00:00.000', '2026-11-01T00:00:00.000', '2026-12-01T00:00:00.000',
        '2027-01-01T00:00:00.000', '2027-02-01T00:00:00.000'
    );
    PRINT 'Partition function pf_AuditEvent_Monthly created.';
END
ELSE
    PRINT 'Partition function pf_AuditEvent_Monthly already exists — skip.';
GO

IF NOT EXISTS (SELECT 1 FROM sys.partition_schemes WHERE name = 'ps_AuditEvent_Monthly')
BEGIN
    CREATE PARTITION SCHEME ps_AuditEvent_Monthly
    AS PARTITION pf_AuditEvent_Monthly ALL TO ([AUDIT_HOT]);
    PRINT 'Partition scheme ps_AuditEvent_Monthly created.';
END
ELSE
    PRINT 'Partition scheme ps_AuditEvent_Monthly already exists — skip.';
GO

IF NOT EXISTS (SELECT 1 FROM sys.partition_functions WHERE name = 'pf_SecurityEvent_Monthly')
BEGIN
    CREATE PARTITION FUNCTION pf_SecurityEvent_Monthly (DATETIME2(3))
    AS RANGE RIGHT FOR VALUES (
        '2026-01-01T00:00:00.000', '2026-02-01T00:00:00.000', '2026-03-01T00:00:00.000',
        '2026-04-01T00:00:00.000', '2026-05-01T00:00:00.000', '2026-06-01T00:00:00.000',
        '2026-07-01T00:00:00.000', '2026-08-01T00:00:00.000', '2026-09-01T00:00:00.000',
        '2026-10-01T00:00:00.000', '2026-11-01T00:00:00.000', '2026-12-01T00:00:00.000',
        '2027-01-01T00:00:00.000', '2027-02-01T00:00:00.000'
    );
    PRINT 'Partition function pf_SecurityEvent_Monthly created.';
END
ELSE
    PRINT 'Partition function pf_SecurityEvent_Monthly already exists — skip.';
GO

IF NOT EXISTS (SELECT 1 FROM sys.partition_schemes WHERE name = 'ps_SecurityEvent_Monthly')
BEGIN
    CREATE PARTITION SCHEME ps_SecurityEvent_Monthly
    AS PARTITION pf_SecurityEvent_Monthly ALL TO ([AUDIT_HOT]);
    PRINT 'Partition scheme ps_SecurityEvent_Monthly created.';
END
ELSE
    PRINT 'Partition scheme ps_SecurityEvent_Monthly already exists — skip.';
GO

-- ═══════════════════════════════════════════════════════════════════════
-- 3. dbo.AuditEvent (eventos de dominio, retention 10 años)
-- ═══════════════════════════════════════════════════════════════════════

IF OBJECT_ID(N'dbo.AuditEvent', N'U') IS NULL
BEGIN
    CREATE TABLE dbo.AuditEvent (
        Id            BIGINT IDENTITY(1,1) NOT NULL,
        OccurredAt    DATETIME2(3)       NOT NULL CONSTRAINT DF_AuditEvent_OccurredAt DEFAULT(SYSUTCDATETIME()),
        ActorUserId   INT                NULL,             -- NULL solo para eventos del sistema (jobs)
        ActorRoleId   INT                NULL,             -- rol efectivo al momento del evento (denormalizado)
        Action        VARCHAR(100)       NOT NULL,         -- "usuario.create", "cliente.deactivate"
        TargetType    VARCHAR(50)        NOT NULL,         -- "Usuario", "Cliente", "Factura"
        TargetId      VARCHAR(100)       NOT NULL,         -- PK del target como string (soporta INT/GUID)
        CorrelationId UNIQUEIDENTIFIER   NULL,             -- linkea eventos de una misma operación
        IpAddress     VARCHAR(45)        NULL,             -- IPv4/IPv6
        UserAgent     VARCHAR(500)       NULL,
        Metadata      NVARCHAR(MAX)      NULL,             -- JSON libre (ya sanitizado por la app)
        CONSTRAINT PK_AuditEvent PRIMARY KEY CLUSTERED (OccurredAt, Id)
            ON ps_AuditEvent_Monthly(OccurredAt),
        CONSTRAINT CK_AuditEvent_Action   CHECK (Action LIKE '%.%'),
        CONSTRAINT CK_AuditEvent_Metadata CHECK (Metadata IS NULL OR ISJSON(Metadata) = 1)
    ) ON ps_AuditEvent_Monthly(OccurredAt);
    PRINT 'Table dbo.AuditEvent created (partitioned monthly on OccurredAt).';
END
ELSE
    PRINT 'Table dbo.AuditEvent already exists — skip.';
GO

-- Índices (cubren 95% de queries: actor / target / action / correlation)
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_AuditEvent_Actor' AND object_id = OBJECT_ID('dbo.AuditEvent'))
BEGIN
    CREATE NONCLUSTERED INDEX IX_AuditEvent_Actor
        ON dbo.AuditEvent(ActorUserId, OccurredAt DESC)
        INCLUDE (Action, TargetType, TargetId, CorrelationId)
        WITH (DATA_COMPRESSION = PAGE)
        ON ps_AuditEvent_Monthly(OccurredAt);
    PRINT 'Index IX_AuditEvent_Actor created.';
END
GO

IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_AuditEvent_Target' AND object_id = OBJECT_ID('dbo.AuditEvent'))
BEGIN
    CREATE NONCLUSTERED INDEX IX_AuditEvent_Target
        ON dbo.AuditEvent(TargetType, TargetId, OccurredAt DESC)
        INCLUDE (ActorUserId, Action, CorrelationId)
        WITH (DATA_COMPRESSION = PAGE)
        ON ps_AuditEvent_Monthly(OccurredAt);
    PRINT 'Index IX_AuditEvent_Target created.';
END
GO

IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_AuditEvent_Action' AND object_id = OBJECT_ID('dbo.AuditEvent'))
BEGIN
    CREATE NONCLUSTERED INDEX IX_AuditEvent_Action
        ON dbo.AuditEvent(Action, OccurredAt DESC)
        INCLUDE (ActorUserId, TargetType, TargetId)
        WITH (DATA_COMPRESSION = PAGE)
        ON ps_AuditEvent_Monthly(OccurredAt);
    PRINT 'Index IX_AuditEvent_Action created.';
END
GO

IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_AuditEvent_Correlation' AND object_id = OBJECT_ID('dbo.AuditEvent'))
BEGIN
    CREATE NONCLUSTERED INDEX IX_AuditEvent_Correlation
        ON dbo.AuditEvent(CorrelationId)
        WHERE CorrelationId IS NOT NULL
        ON ps_AuditEvent_Monthly(OccurredAt);
    PRINT 'Index IX_AuditEvent_Correlation (filtered) created.';
END
GO

-- ═══════════════════════════════════════════════════════════════════════
-- 4. dbo.SecurityEvent (eventos de seguridad, retention 5 años)
-- ═══════════════════════════════════════════════════════════════════════

IF OBJECT_ID(N'dbo.SecurityEvent', N'U') IS NULL
BEGIN
    CREATE TABLE dbo.SecurityEvent (
        Id                BIGINT IDENTITY(1,1) NOT NULL,
        OccurredAt        DATETIME2(3)     NOT NULL CONSTRAINT DF_SecurityEvent_OccurredAt DEFAULT(SYSUTCDATETIME()),
        ActorUserId       INT              NULL,         -- NULL si login falló y user no existe
        AttemptedUsername VARCHAR(256)     NULL,         -- para login failures
        SessionId         UNIQUEIDENTIFIER NULL,
        Action            VARCHAR(100)     NOT NULL,     -- "login", "logout", "refresh.reuse_detected", "permission.denied"
        Result            VARCHAR(20)      NOT NULL,     -- "success" | "failure"
        FailureReason     VARCHAR(200)     NULL,         -- "[REDACTED]        IpAddress         VARCHAR(45)      NULL,
        UserAgent         VARCHAR(500)     NULL,
        Metadata          NVARCHAR(MAX)    NULL,
        CONSTRAINT PK_SecurityEvent PRIMARY KEY CLUSTERED (OccurredAt, Id)
            ON ps_SecurityEvent_Monthly(OccurredAt),
        CONSTRAINT CK_SecurityEvent_Result   CHECK (Result IN ('success','failure')),
        CONSTRAINT CK_SecurityEvent_Metadata CHECK (Metadata IS NULL OR ISJSON(Metadata) = 1)
    ) ON ps_SecurityEvent_Monthly(OccurredAt);
    PRINT 'Table dbo.SecurityEvent created (partitioned monthly on OccurredAt).';
END
ELSE
    PRINT 'Table dbo.SecurityEvent already exists — skip.';
GO

IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_SecurityEvent_Actor' AND object_id = OBJECT_ID('dbo.SecurityEvent'))
BEGIN
    CREATE NONCLUSTERED INDEX IX_SecurityEvent_Actor
        ON dbo.SecurityEvent(ActorUserId, OccurredAt DESC)
        INCLUDE (Action, Result, SessionId)
        WITH (DATA_COMPRESSION = PAGE)
        ON ps_SecurityEvent_Monthly(OccurredAt);
    PRINT 'Index IX_SecurityEvent_Actor created.';
END
GO

IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_SecurityEvent_Action_Result' AND object_id = OBJECT_ID('dbo.SecurityEvent'))
BEGIN
    CREATE NONCLUSTERED INDEX IX_SecurityEvent_Action_Result
        ON dbo.SecurityEvent(Action, Result, OccurredAt DESC)
        INCLUDE (ActorUserId, IpAddress)
        WITH (DATA_COMPRESSION = PAGE)
        ON ps_SecurityEvent_Monthly(OccurredAt);
    PRINT 'Index IX_SecurityEvent_Action_Result created.';
END
GO

IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_SecurityEvent_Ip_Failure' AND object_id = OBJECT_ID('dbo.SecurityEvent'))
BEGIN
    CREATE NONCLUSTERED INDEX IX_SecurityEvent_Ip_Failure
        ON dbo.SecurityEvent(IpAddress, OccurredAt DESC)
        WHERE Result = 'failure'
        ON ps_SecurityEvent_Monthly(OccurredAt);
    PRINT 'Index IX_SecurityEvent_Ip_Failure (filtered) created.';
END
GO

-- ═══════════════════════════════════════════════════════════════════════
-- 5. SYSTEM_VERSIONING — Usuario, Rol, Permiso, RolPermiso
-- ═══════════════════════════════════════════════════════════════════════
-- Patrón: (a) agregar PERIOD FOR SYSTEM_TIME con columnas HIDDEN,
--         (b) activar SYSTEM_VERSIONING con HISTORY_RETENTION_PERIOD 10 YEARS,
--         (c) rebuild history con PAGE compression.
-- Tablas con datos: los registros existentes reciben ValidFrom = instante del ALTER.

-- ─── Usuario ───────────────────────────────────────────────────────────
IF COL_LENGTH('dbo.Usuario', 'ValidFrom') IS NULL
BEGIN
    ALTER TABLE dbo.Usuario
    ADD
        ValidFrom DATETIME2(3) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL
            CONSTRAINT DF_Usuario_ValidFrom DEFAULT(SYSUTCDATETIME()),
        ValidTo   DATETIME2(3) GENERATED ALWAYS AS ROW END   HIDDEN NOT NULL
            CONSTRAINT DF_Usuario_ValidTo   DEFAULT(CONVERT(DATETIME2(3), '9999-12-31 23:59:59.999')),
        PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo);
    PRINT 'Usuario: PERIOD FOR SYSTEM_TIME added.';
END
GO

IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('dbo.Usuario') AND temporal_type = 2)
BEGIN
    ALTER TABLE dbo.Usuario
    SET (SYSTEM_VERSIONING = ON (
        HISTORY_TABLE = dbo.Usuario_History,
        HISTORY_RETENTION_PERIOD = 10 YEARS
    ));
    PRINT 'Usuario: SYSTEM_VERSIONING = ON (history: dbo.Usuario_History, retention: 10 years).';
END
ELSE
    PRINT 'Usuario: SYSTEM_VERSIONING already ON — skip.';
GO

IF EXISTS (SELECT 1 FROM sys.tables WHERE name = 'Usuario_History' AND schema_id = SCHEMA_ID('dbo'))
   AND NOT EXISTS (
       SELECT 1 FROM sys.partitions p
       JOIN sys.tables t ON t.object_id = p.object_id
       WHERE t.name = 'Usuario_History' AND p.data_compression = 2
   )
BEGIN
    ALTER TABLE dbo.Usuario_History REBUILD WITH (DATA_COMPRESSION = PAGE);
    PRINT 'Usuario_History: rebuilt with PAGE compression.';
END
GO

-- ─── Rol ───────────────────────────────────────────────────────────────
IF COL_LENGTH('dbo.Rol', 'ValidFrom') IS NULL
BEGIN
    ALTER TABLE dbo.Rol
    ADD
        ValidFrom DATETIME2(3) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL
            CONSTRAINT DF_Rol_ValidFrom DEFAULT(SYSUTCDATETIME()),
        ValidTo   DATETIME2(3) GENERATED ALWAYS AS ROW END   HIDDEN NOT NULL
            CONSTRAINT DF_Rol_ValidTo   DEFAULT(CONVERT(DATETIME2(3), '9999-12-31 23:59:59.999')),
        PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo);
    PRINT 'Rol: PERIOD FOR SYSTEM_TIME added.';
END
GO

IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('dbo.Rol') AND temporal_type = 2)
BEGIN
    ALTER TABLE dbo.Rol
    SET (SYSTEM_VERSIONING = ON (
        HISTORY_TABLE = dbo.Rol_History,
        HISTORY_RETENTION_PERIOD = 10 YEARS
    ));
    PRINT 'Rol: SYSTEM_VERSIONING = ON.';
END
GO

IF EXISTS (SELECT 1 FROM sys.tables WHERE name = 'Rol_History' AND schema_id = SCHEMA_ID('dbo'))
   AND NOT EXISTS (
       SELECT 1 FROM sys.partitions p JOIN sys.tables t ON t.object_id = p.object_id
       WHERE t.name = 'Rol_History' AND p.data_compression = 2
   )
BEGIN
    ALTER TABLE dbo.Rol_History REBUILD WITH (DATA_COMPRESSION = PAGE);
    PRINT 'Rol_History: rebuilt with PAGE compression.';
END
GO

-- ─── Permiso ───────────────────────────────────────────────────────────
IF COL_LENGTH('dbo.Permiso', 'ValidFrom') IS NULL
BEGIN
    ALTER TABLE dbo.Permiso
    ADD
        ValidFrom DATETIME2(3) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL
            CONSTRAINT DF_Permiso_ValidFrom DEFAULT(SYSUTCDATETIME()),
        ValidTo   DATETIME2(3) GENERATED ALWAYS AS ROW END   HIDDEN NOT NULL
            CONSTRAINT DF_Permiso_ValidTo   DEFAULT(CONVERT(DATETIME2(3), '9999-12-31 23:59:59.999')),
        PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo);
    PRINT 'Permiso: PERIOD FOR SYSTEM_TIME added.';
END
GO

IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('dbo.Permiso') AND temporal_type = 2)
BEGIN
    ALTER TABLE dbo.Permiso
    SET (SYSTEM_VERSIONING = ON (
        HISTORY_TABLE = dbo.Permiso_History,
        HISTORY_RETENTION_PERIOD = 10 YEARS
    ));
    PRINT 'Permiso: SYSTEM_VERSIONING = ON.';
END
GO

IF EXISTS (SELECT 1 FROM sys.tables WHERE name = 'Permiso_History' AND schema_id = SCHEMA_ID('dbo'))
   AND NOT EXISTS (
       SELECT 1 FROM sys.partitions p JOIN sys.tables t ON t.object_id = p.object_id
       WHERE t.name = 'Permiso_History' AND p.data_compression = 2
   )
BEGIN
    ALTER TABLE dbo.Permiso_History REBUILD WITH (DATA_COMPRESSION = PAGE);
    PRINT 'Permiso_History: rebuilt with PAGE compression.';
END
GO

-- ─── RolPermiso ────────────────────────────────────────────────────────
IF COL_LENGTH('dbo.RolPermiso', 'ValidFrom') IS NULL
BEGIN
    ALTER TABLE dbo.RolPermiso
    ADD
        ValidFrom DATETIME2(3) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL
            CONSTRAINT DF_RolPermiso_ValidFrom DEFAULT(SYSUTCDATETIME()),
        ValidTo   DATETIME2(3) GENERATED ALWAYS AS ROW END   HIDDEN NOT NULL
            CONSTRAINT DF_RolPermiso_ValidTo   DEFAULT(CONVERT(DATETIME2(3), '9999-12-31 23:59:59.999')),
        PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo);
    PRINT 'RolPermiso: PERIOD FOR SYSTEM_TIME added.';
END
GO

IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('dbo.RolPermiso') AND temporal_type = 2)
BEGIN
    ALTER TABLE dbo.RolPermiso
    SET (SYSTEM_VERSIONING = ON (
        HISTORY_TABLE = dbo.RolPermiso_History,
        HISTORY_RETENTION_PERIOD = 10 YEARS
    ));
    PRINT 'RolPermiso: SYSTEM_VERSIONING = ON.';
END
GO

IF EXISTS (SELECT 1 FROM sys.tables WHERE name = 'RolPermiso_History' AND schema_id = SCHEMA_ID('dbo'))
   AND NOT EXISTS (
       SELECT 1 FROM sys.partitions p JOIN sys.tables t ON t.object_id = p.object_id
       WHERE t.name = 'RolPermiso_History' AND p.data_compression = 2
   )
BEGIN
    ALTER TABLE dbo.RolPermiso_History REBUILD WITH (DATA_COMPRESSION = PAGE);
    PRINT 'RolPermiso_History: rebuilt with PAGE compression.';
END
GO

PRINT '';
PRINT 'V010 applied successfully — audit infrastructure + temporal tables active.';
PRINT 'Next: runs in B2 onwards (Application.Audit abstractions).';
GO
