Skip to content

Base de Datos

◄ Anterior: Arquitectura | Indice | Siguiente: API Endpoints ►


Actualizado 2026-02-24: Las tablas background_jobs y notifications fueron migradas de schemas por sucursal (LEVEL_SUCURSAL) a la base de datos central DB_INI (schema public). Ver ADR-002 Enmienda para la justificacion completa.

Tabla de Contenidos


Ubicacion y conexion

Base de datos: DB_INI (base de datos central del sistema) Schema: publicConexion en PHP: ConnectionManager::getDbal('ini')

Ambas tablas (background_jobs y notifications) residen en la base de datos central, no en schemas de sucursal. Esto permite queries consolidadas sin UNION cross-schema.

Migraciones (directorio migrations/system/):

  • 20260224000001_create_background_jobs_central.php
  • 20260224000002_create_notifications_central.php
  • 20260227000001_job_scope_lock_index_and_constraint_fix.php — Agrega idx_background_jobs_scope_active; corrige chk_background_jobs_schema_format (3-digit → 4-digit caja)

Tupla de identidad multi-tenant

Dado que las tablas son centrales (no aisladas por schema), el aislamiento multi-tenant se garantiza mediante una tupla de 5 campos presente en ambas tablas:

CampoTipoDescripcion
nro_sistemaINTEGER NOT NULLIdentifica el sistema/empresa
user_idINTEGER NOT NULLIdentifica al usuario
dbVARCHAR(100) NOT NULLBase de datos de la empresa (ej: empresa_xyz, empresa_xyz_p)
schemaVARCHAR(50) NOT NULLSchema PostgreSQL de origen (ej: suc0001, suc0001caja001, public)
pruebaBOOLEAN NOT NULL DEFAULT FALSEDistingue modo prueba (true) de modo oficial (false)

Regla critica: Toda query de lectura DEBE filtrar por al menos nro_sistema + user_id para garantizar aislamiento. Los repositorios JobRepository y NotificationRepository encapsulan este patron.


Tabla: background_jobs

Ubicacion: DB_INI.public.background_jobsRepositorio: App\Core\Repositories\JobRepository (conexion getDbal('ini'))


CampoTipoConstraintsDescripcion
idSERIALPRIMARY KEYID unico autogenerado
typeVARCHAR(100)NOT NULLTipo de job (ej: batch_invoicing)
statusVARCHAR(20)NOT NULL DEFAULT 'pending'Estado: pending | running | completed | failed
payloadJSONBNOT NULLDatos necesarios para ejecutar el job
resultJSONBNULLResultado del job (null hasta que complete)
errorTEXTNULLMensaje de error (null si no fallo)
user_idINTEGERNOT NULLID del usuario que creo el job
nro_sistemaINTEGERNOT NULLNumero de sistema/empresa
pruebaBOOLEANNOT NULL DEFAULT FALSEModo prueba (true) o modo oficial (false)
dbVARCHAR(100)NOT NULLBase de datos de la empresa
schemaVARCHAR(50)NOT NULLSchema PostgreSQL donde ejecutar el job
progressDECIMAL(5,2)NOT NULL DEFAULT 0Porcentaje de progreso (0.00 a 100.00)
retry_countINTEGERNOT NULL DEFAULT 0Numero de reintentos ejecutados
max_retriesINTEGERNOT NULL DEFAULT 3Maximo de reintentos permitidos
next_retry_atTIMESTAMPNULLProximo reintento programado
started_atTIMESTAMPNULLFecha de inicio de ejecucion
completed_atTIMESTAMPNULLFecha de finalizacion
created_atTIMESTAMPNOT NULLFecha de creacion

Indices

ColumnasTipoCondicionProposito
user_idBTREE-Jobs por usuario
statusBTREE-Jobs pendientes/running
created_atBTREE-Busquedas por fecha
typeBTREE-Jobs por tipo
(user_id, status)BTREE-Conteo de pendientes por usuario (DOS protection)
(status, retry_count)BTREE-Jobs fallidos con reintentos disponibles
(nro_sistema, user_id, status)BTREE-Filtro multi-tenant principal
progressBTREEWHERE status = 'running'Parcial: progreso de jobs activos
next_retry_atBTREEWHERE status = 'pending' AND next_retry_at IS NOT NULLParcial: scheduler de reintentos
(type, db, schema, status)BTREEWHERE status IN ('pending', 'running')Parcial: lookup scope-activo para guardia de dedup. Ver nota abajo.

Nota: idx_background_jobs_scope_active

Agregado en la migracion 20260227000001_job_scope_lock_index_and_constraint_fix. Proposito:

  1. Optimizacion de query: Acelera findActiveByDbAndRootSchema() que filtra por (type, db, schema) + status activo.
  2. Ventana de race condition (exact-schema): Dos requests concurrentes con (type, db, schema) identico que ambos pasen la guardia PHP antes de que alguno haga commit encontraran el segundo INSERT fallando con violacion de unicidad — solo si el index fuera UNIQUE. Nota: en la implementacion actual es no-UNIQUE (performance optimization), por lo que actua como safety net de query pero NO como garantia de unicidad. La guardia PHP maneja el caso cross-caja (ej: suc0001 vs suc0001caja0001); la unicidad en exact-schema bajo race condition muy estrecha queda como riesgo conocido y de baja probabilidad.

Para deployments en produccion donde zero-downtime es critico: CREATE INDEX CONCURRENTLY puede usarse pero requiere correr fuera de una transaccion Phinx (via psql directo).

Constraints CHECK

sql
-- Status solo valores validos
CONSTRAINT chk_background_jobs_status
  CHECK (status IN ('pending', 'running', 'completed', 'failed'))

-- Schema tiene formato valido de multi-tenant (ACTUALIZADO 2026-02-27: corregido caja[0-9]{3} → {4})
CONSTRAINT chk_background_jobs_schema_format
  CHECK (schema ~ '^(public|suc[0-9]{4}(caja[0-9]{4})?)$')

-- Progreso entre 0 y 100
CONSTRAINT chk_background_jobs_progress
  CHECK (progress >= 0 AND progress <= 100)

-- Retry count no negativo
CONSTRAINT chk_background_jobs_retry_count
  CHECK (retry_count >= 0)

-- Si completed, debe tener completed_at
CONSTRAINT chk_background_jobs_completed_has_timestamp
  CHECK (
    (status = 'completed' AND completed_at IS NOT NULL)
    OR (status != 'completed')
  )

-- Si failed, debe tener error
CONSTRAINT chk_background_jobs_failed_has_error
  CHECK (
    (status = 'failed' AND error IS NOT NULL)
    OR (status != 'failed')
  )

Nota: chk_background_jobs_schema_format — Correccion aplicada en migracion 20260227000001_job_scope_lock_index_and_constraint_fix. El constraint original usaba caja[0-9]{3} (3 digitos) en lugar del patron correcto caja[0-9]{4} (4 digitos). Los workers siempre emitieron nombres de caja de 4 digitos (ej: caja0001), por lo que las filas existentes ya cumplen el patron corregido. El constraint fue dropeado y re-creado con el patron correcto en la misma migracion.


Tabla: notifications

Ubicacion: DB_INI.public.notificationsRepositorio: App\Core\Repositories\NotificationRepository (conexion getDbal('ini'))


CampoTipoConstraintsDescripcion
idSERIALPRIMARY KEYID unico autogenerado
user_idINTEGERNOT NULLUsuario destinatario
nro_sistemaINTEGERNOT NULLNumero de sistema/empresa
pruebaBOOLEANNOT NULL DEFAULT FALSEModo prueba (true) o modo oficial (false)
dbVARCHAR(100)NOT NULLBase de datos de la empresa
schemaVARCHAR(50)NOT NULLSchema PostgreSQL de origen
typeVARCHAR(20)NOT NULLTipo: success | error | info
titleVARCHAR(200)NOT NULLTitulo breve
messageTEXTNOT NULLMensaje detallado
metadataJSONBNULLDatos adicionales (ej: job_id, resultado)
is_readBOOLEANNOT NULL DEFAULT FALSESi fue leida
created_atTIMESTAMPNOT NULLFecha de creacion
read_atTIMESTAMPNULLFecha de lectura

Indices

ColumnasTipoProposito
user_idBTREENotificaciones por usuario
is_readBTREENotificaciones no leidas
created_atBTREEOrdenamiento por fecha
(nro_sistema, user_id, db, schema, prueba, is_read)BTREEFiltro multi-tenant completo con estado de lectura

Constraints CHECK

sql
-- Type solo valores validos
CONSTRAINT chk_notifications_type
  CHECK (type IN ('success', 'error', 'info'))

-- Consistencia is_read / read_at
CONSTRAINT chk_notifications_read_consistency
  CHECK (
    (is_read = TRUE AND read_at IS NOT NULL)
    OR (is_read = FALSE AND read_at IS NULL)
  )

Funcion PostgreSQL: notify_background_job_update()

Proposito: Emitir una notificacion NOTIFY de PostgreSQL cuando un job cambia de estado o progreso, permitiendo que el backend escuche cambios en tiempo real via SSE.

Nombre de la funcion: notify_background_job_update()

Canal NOTIFY: job_updates_{schema}_{id}

El canal incluye el schema del job ademas del ID. Esto permite que los listeners se suscriban a canales especificos por schema, facilitando el filtrado en entornos multi-tenant.

Trigger:

sql
CREATE TRIGGER background_jobs_notify_trigger
AFTER UPDATE ON background_jobs
FOR EACH ROW
WHEN (
    OLD.status IS DISTINCT FROM NEW.status OR
    OLD.progress IS DISTINCT FROM NEW.progress
)
EXECUTE PROCEDURE notify_background_job_update();

El trigger se dispara cuando cambia status O progress (no solo status como en el diseno original).

Funcion:

sql
CREATE OR REPLACE FUNCTION notify_background_job_update()
RETURNS TRIGGER AS $$
BEGIN
    PERFORM pg_notify(
        'job_updates_' || NEW.schema || '_' || NEW.id::text,
        json_build_object(
            'id',       NEW.id,
            'status',   NEW.status,
            'progress', NEW.progress,
            'result',   NEW.result,
            'error',    NEW.error
        )::text
    );
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

Diferencias con el diseno original:

AspectoDiseno originalImplementacion actual
Canal NOTIFYjob_updates_{id}job_updates_{schema}_{id}
Trigger conditionSolo cambio de statusCambio de status O progress
Payload JSONid, status, result, errorid, status, progress, result, error
Nombre funcionnotify_job_update()notify_background_job_update()

Uso en PHP:

php
// El listener se suscribe al canal especifico del schema y job
$pdo->exec("LISTEN job_updates_{$schema}_{$jobId}");

while (true) {
    $notification = pg_get_notify($pdo);
    if ($notification) {
        // Payload incluye: id, status, progress, result, error
        echo "data: {$notification['payload']}\n\n";
        ob_flush(); flush();
    }
}

◄ Anterior: Arquitectura | Indice | Siguiente: API Endpoints ►