-- =============================================================================
-- 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();
