# Política de Cascadas en Foreign Keys - ERP Construcción **Versión:** 1.0.0 **Fecha:** 2026-02-03 **Subtarea:** ST-P3-003 --- ## Resumen Este documento define la política para configurar acciones de CASCADE, SET NULL, RESTRICT y NO ACTION en foreign keys de la base de datos de ERP Construcción. --- ## Opciones Disponibles | Acción | ON DELETE | ON UPDATE | Descripción | |--------|-----------|-----------|-------------| | CASCADE | ✅ | ✅ | Propaga la acción a registros relacionados | | SET NULL | ✅ | ✅ | Establece NULL en la columna FK | | SET DEFAULT | ✅ | ✅ | Establece el valor DEFAULT | | RESTRICT | ✅ | ✅ | Bloquea si hay referencias (antes del commit) | | NO ACTION | ✅ | ✅ | Bloquea si hay referencias (al commit) | --- ## Estado Actual **Patrón dominante:** `ON DELETE CASCADE` El DDL actual usa CASCADE en todas las foreign keys. Esto es apropiado para: - Relaciones padre-hijo donde el hijo no tiene sentido sin el padre - Multi-tenancy (tenant_id → auth.tenants) - Tablas de detalle (items de una orden, líneas de una póliza) --- ## Guía de Decisión ### Usar ON DELETE CASCADE cuando: 1. **Relación de composición** - El hijo no existe sin el padre ```sql -- Items de una orden de compra order_id UUID NOT NULL REFERENCES purchase.orders(id) ON DELETE CASCADE ``` 2. **Multi-tenancy** - Todos los datos del tenant se eliminan juntos ```sql tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE ``` 3. **Tablas de detalle/líneas** - `estimacion_conceptos` → `estimaciones` - `accounting_entry_lines` → `accounting_entries` - `approval_steps` → `approval_instances` 4. **Historial/Logs asociados** - `asset_locations` → `assets` - `document_versions` → `documents` --- ### Usar ON DELETE SET NULL cuando: 1. **Relación opcional** - El registro puede existir sin la referencia ```sql -- Empleado asignado puede irse pero el ticket persiste assigned_to UUID REFERENCES hr.employees(id) ON DELETE SET NULL ``` 2. **Referencias a usuarios** - El usuario se va pero el registro queda ```sql created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL updated_by UUID REFERENCES auth.users(id) ON DELETE SET NULL ``` 3. **Referencias a catálogos opcionales** ```sql category_id UUID REFERENCES catalogs.categories(id) ON DELETE SET NULL ``` --- ### Usar ON DELETE RESTRICT cuando: 1. **Proteger datos críticos** - No permitir borrar si hay referencias ```sql -- No borrar un proyecto si tiene estimaciones proyecto_id UUID NOT NULL REFERENCES construction.proyectos(id) ON DELETE RESTRICT ``` 2. **Entidades maestras con dependencias de negocio** ```sql -- No borrar proveedor si tiene órdenes de compra supplier_id UUID NOT NULL REFERENCES partners.suppliers(id) ON DELETE RESTRICT ``` 3. **Datos contables/legales** ```sql -- No borrar cuenta contable si tiene movimientos account_id UUID NOT NULL REFERENCES finance.chart_of_accounts(id) ON DELETE RESTRICT ``` --- ### Usar NO ACTION cuando: Similar a RESTRICT, pero permite verificaciones diferidas dentro de una transacción. Usar cuando se necesita reordenar operaciones dentro de una transacción. --- ## ON UPDATE ### Recomendación General: NO especificar Las primary keys son UUIDs inmutables. No hay necesidad de propagar cambios. ```sql -- UUID nunca cambia, no necesita ON UPDATE id UUID PRIMARY KEY DEFAULT gen_random_uuid() ``` ### Si se usa código natural como PK (no recomendado): ```sql -- Solo si el código puede cambiar codigo VARCHAR(20) PRIMARY KEY, -- FK con ON UPDATE CASCADE producto_codigo VARCHAR(20) REFERENCES productos(codigo) ON UPDATE CASCADE ``` --- ## Tabla de Referencia Rápida | Tipo de Relación | ON DELETE | Ejemplo | |------------------|-----------|---------| | Tenant → Datos | CASCADE | `tenant_id → auth.tenants` | | Padre → Hijos (composición) | CASCADE | `order → order_items` | | Referencia opcional | SET NULL | `assigned_to → employees` | | Auditoría (created_by) | SET NULL | `created_by → users` | | Entidad maestra protegida | RESTRICT | `proyecto → estimaciones` | | Catálogo con dependencias | RESTRICT | `account → movements` | --- ## Aplicación en Schemas Existentes ### construction | FK | Actual | Recomendado | Razón | |----|--------|-------------|-------| | fraccionamientos.tenant_id | CASCADE | CASCADE ✅ | Multi-tenant | | etapas.fraccionamiento_id | CASCADE | CASCADE ✅ | Composición | | avances.responsable_id | CASCADE | SET NULL | Usuario puede irse | ### estimates | FK | Actual | Recomendado | Razón | |----|--------|-------------|-------| | estimaciones.proyecto_id | CASCADE | RESTRICT | Proteger datos | | conceptos_estimacion.estimacion_id | CASCADE | CASCADE ✅ | Composición | ### finance | FK | Actual | Recomendado | Razón | |----|--------|-------------|-------| | accounting_entries.tenant_id | CASCADE | CASCADE ✅ | Multi-tenant | | entry_lines.entry_id | CASCADE | CASCADE ✅ | Composición | | entry_lines.account_id | - | RESTRICT | No borrar cuenta con movimientos | --- ## Migración Sugerida Para FK que deberían usar SET NULL o RESTRICT en lugar de CASCADE: ```sql -- Ejemplo: Cambiar CASCADE a RESTRICT ALTER TABLE estimates.estimaciones DROP CONSTRAINT estimaciones_proyecto_id_fkey; ALTER TABLE estimates.estimaciones ADD CONSTRAINT estimaciones_proyecto_id_fkey FOREIGN KEY (proyecto_id) REFERENCES construction.proyectos(id) ON DELETE RESTRICT; ``` **Nota:** Evaluar impacto en lógica de aplicación antes de cambiar. --- ## Referencias - [PostgreSQL Foreign Key Constraints](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-FK) - ESTANDAR-DATABASE-PROFESIONAL.md --- *Documentado: 2026-02-03 - ST-P3-003*