feat(bd): V019 crea dbo.ProductPrices + SP + índices (PRD-003)
This commit is contained in:
71
database/migrations/V019_ROLLBACK.sql
Normal file
71
database/migrations/V019_ROLLBACK.sql
Normal file
@@ -0,0 +1,71 @@
|
||||
-- V019_ROLLBACK.sql
|
||||
-- PRD-003: Reversa de V019__create_product_prices.sql.
|
||||
--
|
||||
-- Pasos:
|
||||
-- 1. Deshabilita SYSTEM_VERSIONING en dbo.ProductPrices (requerido antes de DROP TABLE).
|
||||
-- 2. Elimina el PERIOD FOR SYSTEM_TIME y las columnas hidden SysStartTime/SysEndTime.
|
||||
-- 3. Drop de dbo.ProductPrices_History.
|
||||
-- 4. Drop de dbo.ProductPrices (y sus constraints + índices en cascada).
|
||||
-- 5. Drop de dbo.usp_AddProductPrice.
|
||||
--
|
||||
-- ADVERTENCIA: destruye todo el historial de precios. Ejecutar sólo en DEV o TEST.
|
||||
-- Run on: SIGCM2 (dev), SIGCM2_Test_App, SIGCM2_Test_Api.
|
||||
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
SET ANSI_NULLS ON;
|
||||
SET NOCOUNT ON;
|
||||
GO
|
||||
|
||||
-- 1. Deshabilita SYSTEM_VERSIONING (imprescindible antes de DROP TABLE temporal).
|
||||
IF EXISTS (SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('dbo.ProductPrices') AND temporal_type = 2)
|
||||
BEGIN
|
||||
ALTER TABLE dbo.ProductPrices SET (SYSTEM_VERSIONING = OFF);
|
||||
PRINT 'ProductPrices: SYSTEM_VERSIONING = OFF.';
|
||||
END
|
||||
GO
|
||||
|
||||
-- 2. Elimina el PERIOD y las hidden cols (si existen, independientemente del versioning).
|
||||
IF COL_LENGTH('dbo.ProductPrices', 'SysStartTime') IS NOT NULL
|
||||
BEGIN
|
||||
ALTER TABLE dbo.ProductPrices
|
||||
DROP PERIOD FOR SYSTEM_TIME;
|
||||
|
||||
-- Drop default constraints antes de drop de columnas.
|
||||
IF EXISTS (SELECT 1 FROM sys.default_constraints WHERE name = 'DF_ProductPrices_SysStartTime')
|
||||
ALTER TABLE dbo.ProductPrices DROP CONSTRAINT DF_ProductPrices_SysStartTime;
|
||||
IF EXISTS (SELECT 1 FROM sys.default_constraints WHERE name = 'DF_ProductPrices_SysEndTime')
|
||||
ALTER TABLE dbo.ProductPrices DROP CONSTRAINT DF_ProductPrices_SysEndTime;
|
||||
|
||||
ALTER TABLE dbo.ProductPrices DROP COLUMN SysStartTime;
|
||||
ALTER TABLE dbo.ProductPrices DROP COLUMN SysEndTime;
|
||||
PRINT 'ProductPrices: PERIOD + hidden cols dropped.';
|
||||
END
|
||||
GO
|
||||
|
||||
-- 3. Drop de la history table.
|
||||
IF OBJECT_ID(N'dbo.ProductPrices_History', N'U') IS NOT NULL
|
||||
BEGIN
|
||||
DROP TABLE dbo.ProductPrices_History;
|
||||
PRINT 'Table dbo.ProductPrices_History dropped.';
|
||||
END
|
||||
GO
|
||||
|
||||
-- 4. Drop de la tabla principal (constraints + índices en cascada).
|
||||
IF OBJECT_ID(N'dbo.ProductPrices', N'U') IS NOT NULL
|
||||
BEGIN
|
||||
DROP TABLE dbo.ProductPrices;
|
||||
PRINT 'Table dbo.ProductPrices dropped.';
|
||||
END
|
||||
GO
|
||||
|
||||
-- 5. Drop del SP.
|
||||
IF OBJECT_ID(N'dbo.usp_AddProductPrice', N'P') IS NOT NULL
|
||||
BEGIN
|
||||
DROP PROCEDURE dbo.usp_AddProductPrice;
|
||||
PRINT 'Procedure dbo.usp_AddProductPrice dropped.';
|
||||
END
|
||||
GO
|
||||
|
||||
PRINT '';
|
||||
PRINT 'V019 rollback complete — dbo.ProductPrices, dbo.ProductPrices_History, dbo.usp_AddProductPrice removed.';
|
||||
GO
|
||||
196
database/migrations/V019__create_product_prices.sql
Normal file
196
database/migrations/V019__create_product_prices.sql
Normal file
@@ -0,0 +1,196 @@
|
||||
-- V019__create_product_prices.sql
|
||||
-- PRD-003: ProductPrices — historial de precios por Producto con vigencia civil (Cat2).
|
||||
--
|
||||
-- Cambios:
|
||||
-- 1. dbo.ProductPrices (FK Product, SYSTEM_VERSIONING ON, retention 10 años).
|
||||
-- 2. Índices: filtered UQ un único activo; cover compuesto para GetPriceAt.
|
||||
-- 3. SP dbo.usp_AddProductPrice (SERIALIZABLE + UPDLOCK, cierre atómico forward-only).
|
||||
--
|
||||
-- Patrón: V018 (SYSTEM_VERSIONING + PAGE compression).
|
||||
-- Idempotente: seguro para re-ejecutar.
|
||||
-- Reversa: V019_ROLLBACK.sql.
|
||||
-- Run on: SIGCM2 (dev), SIGCM2_Test_App, SIGCM2_Test_Api.
|
||||
--
|
||||
-- Notas:
|
||||
-- - SysStartTime/SysEndTime como nombres de cols HIDDEN (no ValidFrom/ValidTo):
|
||||
-- evita colisión con las business cols PriceValidFrom/PriceValidTo (D1).
|
||||
-- - DECIMAL(12,2) para Price (distinto de Product.BasePrice DECIMAL(18,4)) — precios retail
|
||||
-- en pesos con 2 decimales; la diferencia es intencional (D6).
|
||||
-- - Sin seed inicial — Product.BasePrice queda ortogonal como fallback (OQ-B, D8).
|
||||
-- - Forward-only estricto en SP: THROW 50409 si new PVF <= active PVF (no solo <).
|
||||
--
|
||||
-- SDD Design: engram sdd/prd-003-product-prices-historicos/design
|
||||
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
SET ANSI_NULLS ON;
|
||||
SET NOCOUNT ON;
|
||||
GO
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
-- 1. dbo.ProductPrices
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
IF OBJECT_ID(N'dbo.ProductPrices', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.ProductPrices (
|
||||
Id BIGINT IDENTITY(1,1) NOT NULL
|
||||
CONSTRAINT PK_ProductPrices PRIMARY KEY,
|
||||
ProductId INT NOT NULL,
|
||||
Price DECIMAL(12,2) NOT NULL,
|
||||
PriceValidFrom DATE NOT NULL,
|
||||
PriceValidTo DATE NULL,
|
||||
FechaCreacion DATETIME2(3) NOT NULL
|
||||
CONSTRAINT DF_ProductPrices_FechaCreacion DEFAULT(SYSUTCDATETIME()),
|
||||
CONSTRAINT FK_ProductPrices_Product
|
||||
FOREIGN KEY (ProductId) REFERENCES dbo.Product(Id) ON DELETE NO ACTION,
|
||||
CONSTRAINT CK_ProductPrices_Price_Positive
|
||||
CHECK (Price > 0),
|
||||
CONSTRAINT CK_ProductPrices_ValidRange
|
||||
CHECK (PriceValidTo IS NULL OR PriceValidTo >= PriceValidFrom)
|
||||
);
|
||||
PRINT 'Table dbo.ProductPrices created.';
|
||||
END
|
||||
ELSE
|
||||
PRINT 'Table dbo.ProductPrices already exists — skip.';
|
||||
GO
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
-- 2. SYSTEM_VERSIONING — ProductPrices
|
||||
-- Las hidden cols se llaman SysStartTime/SysEndTime para evitar
|
||||
-- colisión con las business cols PriceValidFrom/PriceValidTo (D1).
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
IF COL_LENGTH('dbo.ProductPrices', 'SysStartTime') IS NULL
|
||||
BEGIN
|
||||
ALTER TABLE dbo.ProductPrices
|
||||
ADD
|
||||
SysStartTime DATETIME2(3) GENERATED ALWAYS AS ROW START HIDDEN NOT NULL
|
||||
CONSTRAINT DF_ProductPrices_SysStartTime DEFAULT(SYSUTCDATETIME()),
|
||||
SysEndTime DATETIME2(3) GENERATED ALWAYS AS ROW END HIDDEN NOT NULL
|
||||
CONSTRAINT DF_ProductPrices_SysEndTime DEFAULT(CONVERT(DATETIME2(3),'9999-12-31 23:59:59.999')),
|
||||
PERIOD FOR SYSTEM_TIME (SysStartTime, SysEndTime);
|
||||
PRINT 'ProductPrices: PERIOD FOR SYSTEM_TIME added (SysStartTime/SysEndTime).';
|
||||
END
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('dbo.ProductPrices') AND temporal_type = 2)
|
||||
BEGIN
|
||||
ALTER TABLE dbo.ProductPrices
|
||||
SET (SYSTEM_VERSIONING = ON (
|
||||
HISTORY_TABLE = dbo.ProductPrices_History,
|
||||
HISTORY_RETENTION_PERIOD = 10 YEARS
|
||||
));
|
||||
PRINT 'ProductPrices: SYSTEM_VERSIONING = ON (history: dbo.ProductPrices_History, retention: 10 years).';
|
||||
END
|
||||
ELSE
|
||||
PRINT 'ProductPrices: SYSTEM_VERSIONING already ON — skip.';
|
||||
GO
|
||||
|
||||
IF EXISTS (SELECT 1 FROM sys.tables WHERE name = 'ProductPrices_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 = 'ProductPrices_History' AND p.data_compression = 2
|
||||
)
|
||||
BEGIN
|
||||
ALTER TABLE dbo.ProductPrices_History REBUILD WITH (DATA_COMPRESSION = PAGE);
|
||||
PRINT 'ProductPrices_History: rebuilt with PAGE compression.';
|
||||
END
|
||||
GO
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
-- 3. Índices
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Un único activo por producto (imposibilita violar a nivel BD).
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'UX_ProductPrices_Active' AND object_id = OBJECT_ID('dbo.ProductPrices'))
|
||||
BEGIN
|
||||
CREATE UNIQUE INDEX UX_ProductPrices_Active
|
||||
ON dbo.ProductPrices (ProductId)
|
||||
WHERE PriceValidTo IS NULL;
|
||||
PRINT 'Index UX_ProductPrices_Active created.';
|
||||
END
|
||||
GO
|
||||
|
||||
-- Cover para GetPriceAt / GetByProductIdAsync (ProductId + PriceValidFrom con INCLUDEs).
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_ProductPrices_Lookup' AND object_id = OBJECT_ID('dbo.ProductPrices'))
|
||||
BEGIN
|
||||
CREATE INDEX IX_ProductPrices_Lookup
|
||||
ON dbo.ProductPrices (ProductId, PriceValidFrom DESC)
|
||||
INCLUDE (Price, PriceValidTo);
|
||||
PRINT 'Index IX_ProductPrices_Lookup created.';
|
||||
END
|
||||
GO
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
-- 4. SP — dbo.usp_AddProductPrice
|
||||
-- Cierre atómico forward-only: SERIALIZABLE + UPDLOCK + HOLDLOCK.
|
||||
-- Params de salida: @NewId (BIGINT), @ClosedId (BIGINT — NULL si primer precio).
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
GO
|
||||
CREATE OR ALTER PROCEDURE dbo.usp_AddProductPrice
|
||||
@ProductId INT,
|
||||
@Price DECIMAL(12,2),
|
||||
@PriceValidFrom DATE,
|
||||
@NewId BIGINT OUTPUT,
|
||||
@ClosedId BIGINT OUTPUT
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
SET XACT_ABORT ON;
|
||||
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
|
||||
|
||||
BEGIN TRY
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
-- Validación: producto debe existir y estar activo.
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.Product WITH (NOLOCK) WHERE Id = @ProductId AND IsActive = 1)
|
||||
BEGIN
|
||||
ROLLBACK;
|
||||
THROW 50404, 'Product not found or inactive', 1;
|
||||
END
|
||||
|
||||
-- Lee activo con UPDLOCK + HOLDLOCK — bloquea el range key del filtered index.
|
||||
DECLARE @ActiveId BIGINT, @ActivePVF DATE;
|
||||
SELECT TOP 1
|
||||
@ActiveId = Id,
|
||||
@ActivePVF = PriceValidFrom
|
||||
FROM dbo.ProductPrices WITH (UPDLOCK, HOLDLOCK, ROWLOCK)
|
||||
WHERE ProductId = @ProductId AND PriceValidTo IS NULL;
|
||||
|
||||
-- Forward-only estricto: el nuevo PVF debe ser ESTRICTAMENTE mayor al activo.
|
||||
IF @ActiveId IS NOT NULL AND @PriceValidFrom <= @ActivePVF
|
||||
BEGIN
|
||||
ROLLBACK;
|
||||
THROW 50409, 'ProductPriceForwardOnly: new PriceValidFrom must be > active.PriceValidFrom', 1;
|
||||
END
|
||||
|
||||
-- Cierra el activo previo: PVT = PVF(nuevo) - 1 día.
|
||||
IF @ActiveId IS NOT NULL
|
||||
BEGIN
|
||||
UPDATE dbo.ProductPrices
|
||||
SET PriceValidTo = DATEADD(DAY, -1, @PriceValidFrom)
|
||||
WHERE Id = @ActiveId;
|
||||
SET @ClosedId = @ActiveId;
|
||||
END
|
||||
ELSE
|
||||
SET @ClosedId = NULL;
|
||||
|
||||
-- Inserta el nuevo activo.
|
||||
INSERT INTO dbo.ProductPrices (ProductId, Price, PriceValidFrom, PriceValidTo)
|
||||
VALUES (@ProductId, @Price, @PriceValidFrom, NULL);
|
||||
SET @NewId = SCOPE_IDENTITY();
|
||||
|
||||
COMMIT TRANSACTION;
|
||||
END TRY
|
||||
BEGIN CATCH
|
||||
IF XACT_STATE() <> 0 ROLLBACK TRANSACTION;
|
||||
THROW;
|
||||
END CATCH
|
||||
END
|
||||
GO
|
||||
|
||||
PRINT '';
|
||||
PRINT 'V019 applied — dbo.ProductPrices (temporal, retention 10y) + UX_ProductPrices_Active + IX_ProductPrices_Lookup + usp_AddProductPrice.';
|
||||
PRINT 'Next migration: V020 (TBD).';
|
||||
GO
|
||||
Reference in New Issue
Block a user