-- V016__create_rubro.sql -- CAT-001: Árbol N-ario de Rubros — tabla fundacional del catálogo comercial. -- -- Cambios: -- 1. dbo.Rubro (adjacency list, self-FK, soft-delete, SYSTEM_VERSIONING ON, retention 10 años). -- 2. Índice filtrado unique UQ_Rubro_ParentId_Nombre_Activo (unicidad CI por padre en activos). -- 3. Índice cubriente IX_Rubro_ParentId_Activo (child lookups ordenados). -- 4. Permiso 'catalogo:rubros:gestionar' + asignación a rol 'admin'. -- -- Patrón: V011 (dbo.Medio con SYSTEM_VERSIONING + PAGE compression + MERGE permisos). -- Idempotente: seguro para re-ejecutar. -- Reversa: V016_ROLLBACK.sql. -- Run on: SIGCM2 (dev) y SIGCM2_Test (integration tests). -- -- Notas: -- - TarifarioBaseId es INT NULL SIN FK — la FK se agrega en PRC-001. -- - UQ_Rubro_ParentId_Nombre_Activo cubre solo ParentId IS NOT NULL; -- para roots (ParentId IS NULL) la unicidad CI la garantiza Application -- via ExistsByNombreUnderParentAsync(null, ...) — SQL Server trata NULLs -- como distintos en índices únicos. Ver Design §9 Risk 1. -- - FechaCreacion / FechaModificacion: DATETIME2(3) alineado con Medio/Seccion. -- - ValidFrom / ValidTo: DATETIME2(3) GENERATED ALWAYS HIDDEN (idéntico a V011). -- -- Source of truth: Obsidian/02-ARQUITECTURA-y-TECH-STACK/2.5 📋 Auditoría.md -- SDD Design: engram sdd/cat-001-arbol-nario-rubros/design SET QUOTED_IDENTIFIER ON; SET ANSI_NULLS ON; SET NOCOUNT ON; GO -- ═══════════════════════════════════════════════════════════════════════ -- 1. dbo.Rubro -- ═══════════════════════════════════════════════════════════════════════ IF OBJECT_ID(N'dbo.Rubro', N'U') IS NULL BEGIN CREATE TABLE dbo.Rubro ( Id INT IDENTITY(1,1) NOT NULL CONSTRAINT PK_Rubro PRIMARY KEY, ParentId INT NULL, Nombre NVARCHAR(200) NOT NULL COLLATE SQL_Latin1_General_CP1_CI_AI, Orden INT NOT NULL CONSTRAINT DF_Rubro_Orden DEFAULT(0), Activo BIT NOT NULL CONSTRAINT DF_Rubro_Activo DEFAULT(1), TarifarioBaseId INT NULL, -- FK reservada para PRC-001 (sin constraint por ahora) FechaCreacion DATETIME2(3) NOT NULL CONSTRAINT DF_Rubro_FechaCreacion DEFAULT(SYSUTCDATETIME()), FechaModificacion DATETIME2(3) NULL, CONSTRAINT FK_Rubro_Parent FOREIGN KEY (ParentId) REFERENCES dbo.Rubro(Id) ON DELETE NO ACTION ); PRINT 'Table dbo.Rubro created.'; END ELSE PRINT 'Table dbo.Rubro already exists — skip.'; GO -- ═══════════════════════════════════════════════════════════════════════ -- 2. SYSTEM_VERSIONING — Rubro -- ═══════════════════════════════════════════════════════════════════════ IF COL_LENGTH('dbo.Rubro', 'ValidFrom') IS NULL BEGIN ALTER TABLE dbo.Rubro ADD ValidFrom DATETIME2(3) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL CONSTRAINT DF_Rubro_ValidFrom DEFAULT(SYSUTCDATETIME()), ValidTo DATETIME2(3) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL CONSTRAINT DF_Rubro_ValidTo DEFAULT(CONVERT(DATETIME2(3), '9999-12-31 23:59:59.999')), PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo); PRINT 'Rubro: PERIOD FOR SYSTEM_TIME added.'; END GO IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('dbo.Rubro') AND temporal_type = 2) BEGIN ALTER TABLE dbo.Rubro SET (SYSTEM_VERSIONING = ON ( HISTORY_TABLE = dbo.Rubro_History, HISTORY_RETENTION_PERIOD = 10 YEARS )); PRINT 'Rubro: SYSTEM_VERSIONING = ON (history: dbo.Rubro_History, retention: 10 years).'; END ELSE PRINT 'Rubro: SYSTEM_VERSIONING already ON — skip.'; GO IF EXISTS (SELECT 1 FROM sys.tables WHERE name = 'Rubro_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 = 'Rubro_History' AND p.data_compression = 2 ) BEGIN ALTER TABLE dbo.Rubro_History REBUILD WITH (DATA_COMPRESSION = PAGE); PRINT 'Rubro_History: rebuilt with PAGE compression.'; END GO -- ═══════════════════════════════════════════════════════════════════════ -- 3. Índices -- ═══════════════════════════════════════════════════════════════════════ -- Unicidad CI por nombre bajo el mismo padre (solo filas activas + ParentId NOT NULL). -- Para roots (ParentId IS NULL) la unicidad la garantiza Application layer. IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'UQ_Rubro_ParentId_Nombre_Activo' AND object_id = OBJECT_ID('dbo.Rubro')) BEGIN CREATE UNIQUE INDEX UQ_Rubro_ParentId_Nombre_Activo ON dbo.Rubro(ParentId, Nombre) WHERE Activo = 1 AND ParentId IS NOT NULL; PRINT 'Index UQ_Rubro_ParentId_Nombre_Activo created.'; END GO -- Cubriente para child lookups ordenados por Orden. IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_Rubro_ParentId_Activo' AND object_id = OBJECT_ID('dbo.Rubro')) BEGIN CREATE INDEX IX_Rubro_ParentId_Activo ON dbo.Rubro(ParentId, Activo) INCLUDE (Nombre, Orden); PRINT 'Index IX_Rubro_ParentId_Activo created.'; END GO -- ═══════════════════════════════════════════════════════════════════════ -- 4. Permiso: catalogo:rubros:gestionar + asignación a rol 'admin' -- ═══════════════════════════════════════════════════════════════════════ MERGE dbo.Permiso AS t USING (VALUES ('catalogo:rubros:gestionar', N'Gestionar rubros del catálogo', N'Crear, editar, mover y desactivar rubros del árbol de catálogo comercial', 'catalogo') ) AS s (Codigo, Nombre, Descripcion, Modulo) ON t.Codigo = s.Codigo WHEN NOT MATCHED BY TARGET THEN INSERT (Codigo, Nombre, Descripcion, Modulo) VALUES (s.Codigo, s.Nombre, s.Descripcion, s.Modulo); GO MERGE dbo.RolPermiso AS t USING ( SELECT r.Id AS RolId, p.Id AS PermisoId FROM (VALUES ('admin', 'catalogo:rubros:gestionar') ) AS x (RolCodigo, PermisoCodigo) JOIN dbo.Rol r ON r.Codigo = x.RolCodigo JOIN dbo.Permiso p ON p.Codigo = x.PermisoCodigo ) AS s ON t.RolId = s.RolId AND t.PermisoId = s.PermisoId WHEN NOT MATCHED BY TARGET THEN INSERT (RolId, PermisoId) VALUES (s.RolId, s.PermisoId); GO PRINT ''; PRINT 'V016 applied successfully — dbo.Rubro (temporal, retention 10y) + permiso catalogo:rubros:gestionar.'; PRINT 'Next: V017 (future — TBD by next UDT).'; GO