-- ============================================================================= -- OMNIOIL — ESQUEMA MAESTRO DE BASE DE DATOS V4.0 -- PostgreSQL 15+ con TimescaleDB -- ============================================================================= -- -- JERARQUÍA PRINCIPAL: -- CAMPO(edge_projects) → RECURSO(assets) → DISPOSITIVO(edge_devices) → VARIABLE(edge_variables) -- -- CONVENCIÓN DE NOMBRES: -- Nombres ANH originales → Equivalente implementación -- ────────────────────── ────────────────────────────── -- CAMPOS → edge_projects -- RECURSOS → assets -- USUARIOS → users -- ROLES + PERMISOS → roles (JSONB) -- USUARIOS_CAMPOS → user_project_access -- DATOS_CRUDOS → telemetry_raw -- LOGS_AUDITORIA → audit_logs -- REPORTES_JSON_ANH → reportes_json_anh -- REFRESH_TOKENS → refresh_tokens -- (NUEVO) edge_devices → Controladores industriales -- (NUEVO) edge_variables → Puntos de datos SCADA -- (NUEVO) edge_deployments → Historial de despliegues Node-RED -- (NUEVO) edge_agent_logs → Logs del agente IoT -- (NUEVO) operations_movements → Movimientos en tanque -- (NUEVO) operations_downtime → Paradas de pozo -- (NUEVO) lab_results → Resultados de laboratorio -- -- ============================================================================= CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- ╔═══════════════════════════════════════════════════════════════════════════╗ -- ║ MÓDULO 1: SEGURIDAD Y ACCESO ║ -- ║ Origen ANH: USUARIOS, ROLES, PERMISOS, ROLES_PERMISOS, REFRESH_TOKENS ║ -- ╚═══════════════════════════════════════════════════════════════════════════╝ -- Roles del sistema. -- El modelo ANH original usa tablas separadas ROLES + PERMISOS + ROLES_PERMISOS. -- Esta implementación los consolida en una sola tabla con JSONB para los permisos, -- lo cual equivale al JOIN ROLES_PERMISOS pero es más eficiente para RBAC simple. -- Los códigos ANH (ADMIN, SUPERVISOR, OPERADOR, ANH_CONSULTA) se mapean a: -- admin = ADMIN + SUPERVISOR -- operador = OPERADOR -- anh_reader = ANH_CONSULTA CREATE TABLE roles ( id SERIAL PRIMARY KEY, name VARCHAR(50) UNIQUE NOT NULL, -- Permisos granulares (equivale a PERMISOS.codigo) -- Ej: {"SYNC_WRITE": true, "DATA_READ": true, "CAMPO_READ": true, "USERS_ADMIN": false} permissions JSONB NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW() ); INSERT INTO roles (name, permissions) VALUES ('admin', '{"SYNC_WRITE": true, "DATA_READ": true, "CAMPO_READ": true, "USERS_ADMIN": true}'::jsonb), ('supervisor', '{"SYNC_WRITE": true, "DATA_READ": true, "CAMPO_READ": true, "USERS_ADMIN": false}'::jsonb), ('operador', '{"SYNC_WRITE": true, "DATA_READ": true, "CAMPO_READ": false, "USERS_ADMIN": false}'::jsonb), ('anh_reader', '{"DATA_READ": true, "CAMPO_READ": true}'::jsonb) ON CONFLICT (name) DO NOTHING; -- Usuarios del sistema. -- Equivale a la tabla USUARIOS del modelo ANH. -- Cambios vs ANH: "nombre" → "full_name", "activo" → "is_active", -- "ultimo_acceso_ip" → almacenado en audit_logs CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), email VARCHAR(100) UNIQUE NOT NULL, full_name VARCHAR(200), -- ANH: "nombre" password_hash VARCHAR(255) NOT NULL, -- ANH: "password_hash" (sin cambios) role_id INT NOT NULL REFERENCES roles(id), -- FK obligatoria (antes era implícita vía USUARIOS_CAMPOS) is_active BOOLEAN DEFAULT true, -- ANH: "activo" last_login TIMESTAMPTZ, -- ANH: "last_login_at" created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- Tokens de refresco para sesiones. -- Equivale a la tabla REFRESH_TOKENS del modelo ANH (sin cambios). CREATE TABLE refresh_tokens ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), usuario_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, token_hash VARCHAR(255) NOT NULL, expires_at TIMESTAMPTZ NOT NULL, revoked_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW() ); -- ╔═══════════════════════════════════════════════════════════════════════════╗ -- ║ MÓDULO 2: JERARQUÍA OPERATIVA ║ -- ║ CAMPO → RECURSO(Asset) → DISPOSITIVO(Device) → VARIABLE ║ -- ║ Origen ANH: CAMPOS, RECURSOS + extensiones IoT ║ -- ╚═══════════════════════════════════════════════════════════════════════════╝ -- NIVEL 1: CAMPOS / PROYECTOS. -- Equivale a la tabla CAMPOS del modelo ANH. -- Cambios vs ANH: "nombre_campo" → "name", "nombre_operador" → "client", -- "numero_contrato" → almacenado en metadata, -- "ubicacion" → almacenado en metadata, -- "estado" → "is_active" CREATE TABLE edge_projects ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), name VARCHAR(100) NOT NULL, -- ANH: "nombre_campo" client VARCHAR(150) NOT NULL, -- ANH: "nombre_operador" description TEXT, -- Metadata flexible: numero_contrato, ubicacion, region, coordenadas, etc. -- Ej: {"numero_contrato": "C-12345", "ubicacion": "Llanos Orientales", "departamento": "Meta"} metadata JSONB DEFAULT '{}'::jsonb, is_active BOOLEAN DEFAULT true, -- ANH: "estado" created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_edge_projects_client ON edge_projects(client); -- Acceso granular de usuarios a campos/proyectos. -- Equivale a la tabla USUARIOS_CAMPOS del modelo ANH. -- Cambios vs ANH: Se simplifica; el rol global viene de users.role_id. -- El project_role es para permisos ADICIONALES por campo. CREATE TABLE user_project_access ( user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, project_id UUID NOT NULL REFERENCES edge_projects(id) ON DELETE CASCADE, project_role VARCHAR(50) DEFAULT 'viewer', -- Rol dentro de este campo específico is_active BOOLEAN DEFAULT true, -- ANH: "activo" de USUARIOS_CAMPOS created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), PRIMARY KEY (user_id, project_id) ); -- NIVEL 2: RECURSOS / ACTIVOS FÍSICOS. -- Equivale a la tabla RECURSOS del modelo ANH. -- Cambios vs ANH: "nombre_recurso" → "code" + "name", -- "tipo_recurso" → "asset_type" (con CHECK constraint), -- Se agrega "project_id" como FK obligatoria a CAMPOS/edge_projects CREATE TABLE edge_assets ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), project_id UUID NOT NULL REFERENCES edge_projects(id) ON DELETE CASCADE, -- ANH: "campo_id" code VARCHAR(50) UNIQUE NOT NULL, -- Código ANH del recurso (Ej: TK-5001, POZO-X1) name VARCHAR(100) NOT NULL, -- Nombre descriptivo (Ej: "Tanque Principal") asset_type VARCHAR(20) NOT NULL -- ANH: "tipo_recurso" CHECK (asset_type IN ('POZO', 'TANQUE', 'MEDIDOR_GAS', 'AGUA', 'OTRO')), is_active BOOLEAN DEFAULT true, -- ANH: "activo" de RECURSOS -- Metadata: capacidad_max, producto, coordenadas, etc. metadata JSONB DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); COMMENT ON COLUMN edge_assets.code IS 'Ej: TK-5001, POZO-X1, Medidor-Gas-01'; COMMENT ON COLUMN edge_assets.asset_type IS 'POZO | TANQUE | MEDIDOR_GAS | AGUA | OTRO'; CREATE INDEX idx_edge_assets_project ON edge_assets(project_id); CREATE INDEX idx_edge_assets_type ON edge_assets(asset_type); -- NIVEL 3: DISPOSITIVOS DE COMUNICACIÓN INDUSTRIAL (NUEVO — Edge Agent). -- No existe en el modelo ANH original. Necesario para: -- - Flow Generator: genera flows.json para Node-RED -- - Edge Agent: sabe qué contenedor levantar -- - Conexión SCADA: IP/Puerto/Protocolo del PLC/RTU/Gateway CREATE TABLE edge_devices ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), asset_id UUID NOT NULL REFERENCES edge_assets(id) ON DELETE CASCADE, name VARCHAR(100) NOT NULL, -- Ej: "PLC-001 Bomba Principal" protocol VARCHAR(30) NOT NULL CHECK (protocol IN ('MODBUS_TCP', 'MODBUS_RTU', 'OPC_UA', 'MQTT', 'ETHERNET_IP', 'PROFINET')), host VARCHAR(255) NOT NULL, -- IP o hostname del dispositivo port INTEGER NOT NULL, -- Puerto TCP (502 Modbus, 4840 OPC UA) -- Configuración específica del protocolo: -- Modbus: {"unit_id": 1, "timeout_ms": 3000, "retries": 3} -- OPC UA: {"security_policy": "Basic256Sha256", "endpoint_url": "opc.tcp://..."} protocol_config JSONB DEFAULT '{}'::jsonb, is_enabled BOOLEAN DEFAULT true, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_edge_devices_asset ON edge_devices(asset_id); -- NIVEL 4: VARIABLES / PUNTOS DE DATOS SCADA (NUEVO — Flow Generator). -- Cada registro del PLC que se desea leer. Necesario para: -- - Flow Generator: crea un nodo Modbus-Read o OPC-UA por variable -- - Alarmas: define umbrales min/max para alertas en tiempo real -- - Telemetría: nombra las claves dentro del payload JSONB CREATE TABLE edge_variables ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), device_id UUID NOT NULL REFERENCES edge_devices(id) ON DELETE CASCADE, -- Dirección del registro industrial: -- Modbus: "HR:40001" (Holding), "IR:30001" (Input), "COIL:00001", "DI:10001" -- OPC UA: "ns=2;s=Channel1.Device1.Tag1" address VARCHAR(255) NOT NULL, data_type VARCHAR(30) NOT NULL CHECK (data_type IN ('bool', 'int16', 'uint16', 'int32', 'uint32', 'float32', 'float64', 'string')), alias VARCHAR(100) NOT NULL, -- Nombre en el SCADA (Ej: "Presion Cabeza") unit VARCHAR(30), -- Unidad de ingeniería (PSI, °C, BBL, m³) polling_interval_ms INTEGER DEFAULT 5000, -- Alarmas: alarm_min DOUBLE PRECISION, alarm_max DOUBLE PRECISION, alarm_enabled BOOLEAN DEFAULT false, -- Byte order para registros multi-word (Modbus): byte_order VARCHAR(10) DEFAULT 'BE' CHECK (byte_order IN ('BE', 'LE', 'BE_SWAP', 'LE_SWAP')), -- Metadata adicional: {"scale_factor": 0.1, "offset": 0, "description": "..."} metadata JSONB DEFAULT '{}'::jsonb, is_enabled BOOLEAN DEFAULT true, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_edge_variables_device ON edge_variables(device_id); -- ╔═══════════════════════════════════════════════════════════════════════════╗ -- ║ MÓDULO 3: TELEMETRÍA Y DATOS OPERATIVOS ║ -- ║ Origen ANH: DATOS_CRUDOS + extensiones de serie temporal ║ -- ╚═══════════════════════════════════════════════════════════════════════════╝ -- Telemetría de alta frecuencia. -- Equivale/reemplaza a la tabla DATOS_CRUDOS del modelo ANH. -- Cambios vs ANH: Diseño optimizado para TimescaleDB (hypertable). -- - "campo_id" → se infiere vía assets.project_id -- - "recurso_id" → "asset_id" (referencia directa) -- - "usuario_id" → no aplica (los datos llegan del SCADA automáticamente) -- - "tipo_registro" → implícito en asset_type -- - "estado" → no aplica (los datos crudos no tienen estado de validación aquí) -- - "hora_medicion" → "time" (convención TimescaleDB) -- - "datos" + "metadata" → "data" (un solo JSONB con todo el payload) CREATE TABLE telemetry_raw ( time TIMESTAMPTZ NOT NULL, -- ANH: "hora_medicion" asset_id UUID NOT NULL REFERENCES edge_assets(id) ON DELETE CASCADE, -- ANH: "recurso_id" -- Payload completo: -- Pozo: {"presion_cabeza": 120.5, "temperatura_cabeza": 95.0, "corriente_motor": 45.2} -- Tanque: {"nivel_total": 5400, "nivel_agua": 200, "temp_crudo": 85.4} data JSONB NOT NULL, -- ANH: "datos" + "metadata" PRIMARY KEY (time, asset_id) ); -- Hypertable TimescaleDB (si está habilitado) -- SELECT create_hypertable('telemetry_raw', 'time', if_not_exists => TRUE); -- Compresión automática -- ALTER TABLE telemetry_raw SET ( -- timescaledb.compress, -- timescaledb.compress_segmentby = 'asset_id' -- ); -- SELECT add_compression_policy('telemetry_raw', INTERVAL '7 days'); -- Movimientos en tanque (baja frecuencia) — del JSON ANH segmento baja frecuencia. -- Modela: SEGMENTO_DATOS_DE_BAJA_FRECUENCIA.MOVIMIENTOS_EN_TANQUE CREATE TABLE operations_movements ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), asset_id UUID NOT NULL REFERENCES edge_assets(id), movement_type VARCHAR(50) NOT NULL CHECK (movement_type IN ('VENTA', 'RECIBO', 'TRANSFERENCIA', 'QUEMA', 'CONSUMO')), start_time TIMESTAMPTZ NOT NULL, end_time TIMESTAMPTZ NOT NULL, volumen_neto NUMERIC(10, 2), -- Detalles: {"ticket": "W-1234", "cliente": "Ecopetrol", "comentario": "..."} details JSONB DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ DEFAULT NOW() ); -- Paradas de pozo (baja frecuencia) — del JSON ANH segmento baja frecuencia. -- Modela: SEGMENTO_DATOS_DE_BAJA_FRECUENCIA.PARADAS_DE_POZO CREATE TABLE operations_downtime ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), asset_id UUID NOT NULL REFERENCES edge_assets(id), start_time TIMESTAMPTZ NOT NULL, end_time TIMESTAMPTZ, -- NULL si la parada sigue activa is_planned BOOLEAN DEFAULT false, -- ANH: "PARO_PROGRAMADO" (SI/NO) reason VARCHAR(255) NOT NULL, -- ANH: "CAUSA_PARO" comments TEXT, -- ANH: "COMENTARIO" created_at TIMESTAMPTZ DEFAULT NOW() ); -- Resultados de laboratorio — del JSON ANH segmento baja frecuencia. -- Modela las calidades de crudo (BSW, API, cromatografía, etc.) CREATE TABLE lab_results ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), asset_id UUID NOT NULL REFERENCES edge_assets(id), sample_time TIMESTAMPTZ NOT NULL, -- Payload: {"bsw": 0.5, "api": 32.1, "azufre": 0.02, "temp_lab": 60.0} -- O gas: {"metano": 85.5, "etano": 5.2, "co2": 1.5, "poder_calorifico": 1050.2} results JSONB NOT NULL, lab_technician VARCHAR(100), created_at TIMESTAMPTZ DEFAULT NOW() ); -- ╔═══════════════════════════════════════════════════════════════════════════╗ -- ║ MÓDULO 4: EDGE AGENT — DESPLIEGUE Y MONITOREO ║ -- ║ (NUEVO — No existe en modelo ANH original) ║ -- ╚═══════════════════════════════════════════════════════════════════════════╝ -- Historial de despliegues de contenedores Node-RED. -- Cada vez que el Build Orchestrator genera una imagen Docker, se registra aquí. -- El Edge Agent actualiza el status cuando el contenedor arranca o falla. CREATE TABLE edge_deployments ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), project_id UUID NOT NULL REFERENCES edge_projects(id) ON DELETE CASCADE, -- Imagen Docker generada image_tag VARCHAR(500) NOT NULL, -- Ej: registry.local/scada-project:v3 image_hash VARCHAR(100), -- sha256:abc123... -- Para rollback automático previous_image_tag VARCHAR(500), previous_image_hash VARCHAR(100), -- Estado del ciclo de vida status VARCHAR(30) NOT NULL DEFAULT 'BUILDING' CHECK (status IN ('BUILDING', 'BUILT', 'PUSHING', 'PUSHED', 'DEPLOYING', 'DEPLOYED', 'FAILED', 'ROLLED_BACK')), -- Snapshot inmutable de la configuración al momento del build config_snapshot JSONB, -- Metadata: {"build_duration_ms": 12345, "triggered_by": "user@..."} build_metadata JSONB DEFAULT '{}'::jsonb, error_message TEXT, deployed_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_edge_deployments_project ON edge_deployments(project_id); CREATE INDEX idx_edge_deployments_status ON edge_deployments(status); -- Logs centralizados del Edge Agent. -- El agente de borde envía logs vía MQTT al servidor, que los persiste aquí. CREATE TABLE edge_agent_logs ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), project_id UUID NOT NULL REFERENCES edge_projects(id) ON DELETE CASCADE, level VARCHAR(10) NOT NULL CHECK (level IN ('DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL')), category VARCHAR(50) NOT NULL, -- 'DEPLOY', 'HEALTH', 'CONTAINER', 'MQTT', 'SYSTEM' message TEXT NOT NULL, context JSONB DEFAULT '{}'::jsonb, agent_hostname VARCHAR(255), agent_ip INET, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_edge_agent_logs_project ON edge_agent_logs(project_id); CREATE INDEX idx_edge_agent_logs_level ON edge_agent_logs(level); CREATE INDEX idx_edge_agent_logs_created ON edge_agent_logs(created_at DESC); -- Hypertable para logs (si TimescaleDB disponible): -- SELECT create_hypertable('edge_agent_logs', 'created_at', if_not_exists => TRUE); -- ╔═══════════════════════════════════════════════════════════════════════════╗ -- ║ MÓDULO 5: AUDITORÍA Y REPORTES ANH ║ -- ║ Origen ANH: LOGS_AUDITORIA, REPORTES_JSON_ANH ║ -- ╚═══════════════════════════════════════════════════════════════════════════╝ -- Auditoría forense. -- Equivale a LOGS_AUDITORIA del modelo ANH. -- Cambios vs ANH: "tipo_evento" → "action", "entidad_afectada" → "table_name", -- "entidad_id" → "record_id", "dato_anterior/nuevo" → "old_data/new_data", -- "descripcion" → "change_reason" CREATE TABLE audit_logs ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID REFERENCES users(id), -- ANH: "usuario_id" action VARCHAR(50) NOT NULL, -- ANH: "tipo_evento" (INSERT, UPDATE, DELETE) table_name VARCHAR(50) NOT NULL, -- ANH: "entidad_afectada" record_id UUID NOT NULL, -- ANH: "entidad_id" old_data JSONB, -- ANH: "dato_anterior" new_data JSONB, -- ANH: "dato_nuevo" change_reason TEXT, -- ANH: "descripcion" ip_address INET, -- ANH: "ip_origen" created_at TIMESTAMPTZ DEFAULT NOW() ); -- Control de envíos de reportes JSON a la ANH. -- Equivale a REPORTES_JSON_ANH del modelo ANH. -- Cambios vs ANH: "campo_id" → ahora referencia a "assets" para reportes por recurso, -- o dejamos project_id para reportes por campo completo. CREATE TABLE reportes_json_anh ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), project_id UUID REFERENCES edge_projects(id), -- ANH: "campo_id" usuario_id UUID REFERENCES users(id), -- ANH: "usuario_id" (quién generó) fecha_operacional DATE NOT NULL, nombre_archivo VARCHAR(255), estado_envio VARCHAR(30) DEFAULT 'PENDIENTE' CHECK (estado_envio IN ('PENDIENTE', 'GENERADO', 'ENVIADO', 'ACEPTADO', 'RECHAZADO', 'ERROR')), ruta_datalake TEXT, -- Ruta en MinIO/Azure Blob hash_archivo VARCHAR(100), -- SHA256 para integridad created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- ╔═══════════════════════════════════════════════════════════════════════════╗ -- ║ TRIGGERS Y FUNCIONES UTILITARIAS ║ -- ╚═══════════════════════════════════════════════════════════════════════════╝ CREATE OR REPLACE FUNCTION trigger_set_timestamp() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER set_timestamp_users BEFORE UPDATE ON users FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp(); CREATE TRIGGER set_timestamp_projects BEFORE UPDATE ON edge_projects FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp(); CREATE TRIGGER set_timestamp_assets BEFORE UPDATE ON edge_assets FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp(); CREATE TRIGGER set_timestamp_devices BEFORE UPDATE ON edge_devices FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp(); CREATE TRIGGER set_timestamp_variables BEFORE UPDATE ON edge_variables FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp(); CREATE TRIGGER set_timestamp_reportes BEFORE UPDATE ON reportes_json_anh FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp();