Skip to content

Consolidación Inversa de Schemas (Hijo → Padre)

Módulo: Migrations Tipo: Database Seed Fecha creación: 2026-02-11 Versión: v3.13.1 Archivo: /bautista-backend/migrations/seeds/tenancy/ZZSchemaDataConsolidation.php


Overview

El seed ZZSchemaDataConsolidation implementa consolidación inversa de datos entre schemas PostgreSQL, moviendo tablas desde niveles inferiores (hijo) hacia niveles superiores (padre) cuando cambia la configuración de niveles en configuracion_niveles_tablas.

Este proceso complementa el seed AASchemaDataMigration (padre → hijo), creando un sistema bidireccional completo de redistribución de datos según cambios en la configuración del sistema.

Propósito principal: Cuando una tabla configurada originalmente para nivel 2 (sucursal) o nivel 3 (caja) se reconfigura para nivel 1 (empresa), el sistema automáticamente consolida los datos dispersos en múltiples schemas hijo hacia el schema padre, sincroniza el historial de migraciones (phinxlog), y ejecuta nuevamente las migraciones para completar la transición.

Restricción de portabilidad por tipo de migración: Solo las tablas de tipo BASE 🏗️ (tablas base compartidas) son portables entre schemas. Las tablas TRANSACCIONAL 🔄 (transacciones) y CONFIG ⚙️ (configuraciones) NO son portables porque contienen datos específicos por ubicación. Ver docs/backend/migration-types-system.md para detalles completos sobre tipos de migración.


Problem Statement

Contexto del Problema

En el sistema multi-tenant basado en schemas de PostgreSQL, las tablas pueden existir en diferentes niveles jerárquicos:

  • Nivel 1 (EMPRESA): public - Datos compartidos por toda la empresa
  • Nivel 2 (SUCURSAL): suc0001, suc0002, etc. - Datos específicos de sucursal
  • Nivel 3 (CAJA): suc0001caja001, suc0001caja002, etc. - Datos específicos de punto de venta

El campo JSONB configuracion_niveles_tablas en la tabla sistema permite personalizar en qué niveles debe existir cada tabla:

json
{
  "ordcon": [1], // Solo nivel empresa (clientes compartidos)
  "factura": [3], // Solo nivel caja (facturas por punto)
  "movimi": [2, 3] // Niveles sucursal y caja
}

Escenarios que Requieren Consolidación Inversa

Escenario 1: Cambio de Configuración Inicial

Una empresa inicia con clientes (ordcon) distribuidos por sucursal (nivel 2), pero luego decide centralizar:

json
// ANTES
{"ordcon": [1, 2]}

// DESPUÉS
{"ordcon": [1]}

Problema: Existen datos de ordcon en suc0001.ordcon, suc0002.ordcon, etc., pero la nueva configuración requiere que estén únicamente en public.ordcon.

Nota: ordcon es una tabla BASE (portable) - clientes compartidos - por lo que SÍ puede consolidarse. Las tablas TRANSACCIONAL (facturas, movimientos) y CONFIG (configuraciones por sucursal) NO pueden consolidarse porque sus datos están ligados a la ubicación específica.

Escenario 2: Migraciones Skipped

Cuando AASchemaDataMigration detecta que una tabla ya existe en un schema hijo, omite crearla nuevamente. Si luego se cambia la configuración a un nivel superior, esas migraciones quedan "skipped" en el phinxlog del padre, causando errores al ejecutar migraciones futuras.

Escenario 3: Datos Huérfanos

Tablas creadas manualmente o por procesos anteriores que quedaron en schemas hijo y necesitan moverse al padre para centralizar operaciones.

Soluciones Existentes Insuficientes

SoluciónLimitación
Migración manualPropensa a errores, no escala
DROP + re-ejecutar migracionesPérdida de datos
AASchemaDataMigration soloSolo mueve padre → hijo, no inversa
Modificar phinxlog manualmenteRiesgo de inconsistencias

Impacto sin Consolidación Inversa

  1. Datos fragmentados: Información distribuida en múltiples schemas cuando debería estar centralizada
  2. Errores en migraciones: Phinx intenta crear estructuras que "ya existen" en schemas hijos
  3. Inconsistencia de datos: Búsquedas incompletas si las queries solo consultan el schema padre
  4. Complejidad operativa: Personal técnico debe intervenir manualmente cada vez

Architecture

Principios de Diseño

  1. Detección Automática: Compara configuración actual vs. ubicación real de tablas
  2. Validación de Conflictos: Evita consolidaciones cuando hay datos en múltiples schemas hermanos
  3. Transacciones Independientes: Una tabla fallida no bloquea consolidación de otras
  4. Sincronización de Migraciones: Marca migraciones como ejecutadas (simula --fake) para evitar re-ejecuciones
  5. Re-Ejecución Guiada: Crea flag y muestra instrucciones para completar el proceso

Diagrama de Flujo del Proceso

mermaid
flowchart TD
    Start[Inicio: ZZSchemaDataConsolidation] --> DetectCandidates[Fase 1: Detección de Candidatos]

    DetectCandidates --> ReadConfig[Leer configuracion_niveles_tablas]
    ReadConfig --> GetTables[Obtener tablas en schema actual]
    GetTables --> CheckConfig{Tabla tiene<br/>configuración<br/>personalizada?}

    CheckConfig -->|No| NextTable[Siguiente tabla]
    CheckConfig -->|Sí| CheckLevel{Nivel actual<br/>en config?}

    CheckLevel -->|Sí| NextTable
    CheckLevel -->|No| AddCandidate[Agregar a candidatos]

    AddCandidate --> MoreTables{Más tablas?}
    MoreTables -->|Sí| GetTables
    MoreTables -->|No| HasCandidates{Hay candidatos?}

    HasCandidates -->|No| EndNoWork[Fin: Sin tablas para consolidar]
    HasCandidates -->|Sí| Validation[Fase 2: Validación]

    Validation --> ValidateTable[Para cada candidato]
    ValidateTable --> GetSiblings[Obtener schemas hermanos]
    GetSiblings --> CountRecords[Contar registros en cada schema]
    CountRecords --> CheckConflict{Múltiples schemas<br/>con datos?}

    CheckConflict -->|Sí| AddConflict[Registrar conflicto]
    CheckConflict -->|No| AddValidated[Agregar a validados]

    AddConflict --> MoreCandidates1{Más candidatos?}
    AddValidated --> MoreCandidates1
    MoreCandidates1 -->|Sí| ValidateTable
    MoreCandidates1 -->|No| HasConflicts{Hay conflictos?}

    HasConflicts -->|Sí| Abort[Abortar: Mostrar conflictos]
    HasConflicts -->|No| Consolidation[Fase 3: Consolidación]

    Consolidation --> StartTx[Iniciar transacción]
    StartTx --> CreateStructure{Tabla existe<br/>en padre?}

    CreateStructure -->|No| CreateTable[Crear estructura con LIKE]
    CreateStructure -->|Sí| CheckData{Tiene datos?}
    CreateTable --> CheckData

    CheckData -->|Sí| CopyData[Copiar datos con mapeo explícito]
    CheckData -->|No| SyncLog[Sincronizar phinxlog]

    CopyData --> UpdateSeq[Actualizar secuencias]
    UpdateSeq --> SyncLog

    SyncLog --> FilterMigrations[Filtrar migraciones por tabla]
    FilterMigrations --> MarkExecuted[Marcar en phinxlog destino]
    MarkExecuted --> DropSource[Eliminar tabla origen]

    DropSource --> CommitTx[Commit transacción]
    CommitTx --> MoreValidated{Más validados?}

    MoreValidated -->|Sí| StartTx
    MoreValidated -->|No| FlagCreation[Fase 4: Indicador]

    FlagCreation --> CreateFlag[Crear flag en /tmp]
    CreateFlag --> ShowMessage[Mostrar instrucción de re-ejecución]
    ShowMessage --> EndSuccess[Fin: Consolidación completada]

    Abort --> EndConflict[Fin: Conflictos detectados]

Relación con AASchemaDataMigration

mermaid
sequenceDiagram
    participant Config as configuracion_niveles_tablas
    participant AA as AASchemaDataMigration
    participant Schema as PostgreSQL Schemas
    participant ZZ as ZZSchemaDataConsolidation
    participant Phinx as Sistema de Migraciones

    Note over Config: Estado inicial: ordcon en [1,2]
    Note over Schema: ordcon existe en public y suc0001

    Config->>Config: Admin cambia a [1]

    Note over AA: Seed AA ejecuta PRIMERO
    AA->>Schema: Detecta tablas en parent no en child
    AA->>Schema: Migra public → suc0001 (si aplica)

    Note over ZZ: Seed ZZ ejecuta DESPUÉS
    ZZ->>Config: Lee nueva configuración [1]
    ZZ->>Schema: Detecta ordcon en suc0001 pero config dice [1]
    ZZ->>Schema: Valida sin conflictos
    ZZ->>Schema: Consolida suc0001.ordcon → public.ordcon
    ZZ->>Schema: Sincroniza phinxlog
    ZZ->>ZZ: Crea flag de re-ejecución

    Note over ZZ: Flag creado: /tmp/bautista_consolidation_*.flag

    ZZ-->>Phinx: Instrucción: RE-EJECUTAR migraciones

    Note over Phinx: Admin ejecuta: php migrate-db-command.php --migrate
    Phinx->>Schema: Detecta phinxlog sincronizado
    Phinx->>Schema: Ejecuta migraciones pendientes
    Phinx->>Schema: Completa estructuras faltantes

Algorithm (4 Phases)

Fase 1: Detección de Candidatos

Objetivo: Identificar tablas que existen en el schema actual pero cuya configuración indica que deberían estar en un nivel superior.

Método: detectConsolidationCandidates()

Lógica:

  1. Obtener schema actual y su nivel (1, 2, o 3)
  2. Leer configuracion_niveles_tablas desde sistema
  3. Obtener todas las tablas del schema actual (excluyendo phinxlog, migrations, schema_migrations)
  4. Para cada tabla:
    • Si NO tiene configuración personalizada → ignorar (usar defaults)
    • Si nivel actual NO está en configured_levels → candidata para consolidación
    • Determinar schema padre (inmediato superior en jerarquía)
    • Agregar a lista: ['table', 'from_schema', 'to_schema', 'current_level', 'configured_levels']

Ejemplo:

php
// Schema actual: suc0001 (nivel 2)
// Configuración: {"ordcon": [1]}
// Resultado: ordcon existe en suc0001 pero config dice [1]
// → Candidata: ['table' => 'ordcon', 'from_schema' => 'suc0001', 'to_schema' => 'public', ...]

SQL Crítico:

sql
-- Obtener todas las tablas del schema actual
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'suc0001'
  AND table_type = 'BASE TABLE'
  AND table_name NOT IN ('phinxlog', 'migrations', 'schema_migrations')
ORDER BY table_name;

Fase 2: Validación de Conflictos

Objetivo: Evitar consolidaciones cuando múltiples schemas hermanos contienen datos (pérdida de información).

Método: validateTableConsolidation()

Lógica:

  1. Obtener schemas hermanos del mismo nivel:
    • Nivel 2: Otras sucursales (suc0001, suc0002, etc.)
    • Nivel 3: Otras cajas de la misma sucursal (suc0001caja001, suc0001caja002, etc.)
  2. Contar registros en cada schema hermano (incluyendo el actual)
  3. Validar conflictos:
    • 0 schemas con datos: Consolidar (eliminar tablas vacías)
    • 1 schema con datos: Consolidar (mover único conjunto de datos)
    • 2+ schemas con datos: CONFLICTO (requiere intervención manual)

Método auxiliar: getSiblingSchemas()

SQL para schemas hermanos (Nivel 2 - Sucursales):

sql
-- Buscar otras sucursales (nivel 2)
SELECT schema_name
FROM information_schema.schemata
WHERE schema_name ~ '^suc[0-9]+$'      -- Regex: suc + dígitos
  AND schema_name != 'suc0001'         -- Excluir el actual
ORDER BY schema_name;

SQL para schemas hermanos (Nivel 3 - Cajas):

sql
-- Buscar otras cajas de suc0001
SELECT schema_name
FROM information_schema.schemata
WHERE schema_name ~ '^suc0001caja[0-9]+$'   -- Regex: suc0001caja + dígitos
  AND schema_name != 'suc0001caja001'       -- Excluir el actual
ORDER BY schema_name;

Ejemplos de Validación:

Escenariosuc0001suc0002Resultado
Caso 1100 registros0 registrosConsolidar ✅
Caso 20 registros0 registrosConsolidar ✅ (eliminar vacías)
Caso 3100 registros50 registrosCONFLICTO ❌

Fase 3: Consolidación

Objetivo: Mover datos, sincronizar estructura y phinxlog, eliminar origen.

Método: consolidateTableToParent()

Subfases:

A. Verificar/Crear Estructura en Destino

Método: createTableStructureInTarget()

sql
-- Crear tabla en destino copiando estructura completa
CREATE TABLE public.ordcon (
  LIKE suc0001.ordcon INCLUDING ALL
);
-- INCLUDING ALL copia: defaults, constraints, indexes, comments

B. Copiar Datos

Método heredado: migrateDataWithExplicitMapping() (de AutoMigrationSeeder)

Lógica:

  1. Obtener columnas de ambas tablas
  2. Encontrar columnas comunes (intersección)
  3. Construir INSERT con mapeo explícito por nombres
sql
-- Mapeo explícito evita problemas de orden de columnas
INSERT INTO public.ordcon (id, razon, cuit, direccion)
SELECT id, razon, cuit, direccion
FROM suc0001.ordcon
ON CONFLICT DO NOTHING;

C. Actualizar Secuencias

Método heredado: updateTableSequences() (de AutoMigrationSeeder)

sql
-- Obtener valor máximo actual
SELECT COALESCE(MAX(id), 0) FROM public.ordcon;
-- Resultado: 1250

-- Establecer próximo valor de secuencia
SELECT setval('public.ordcon_id_seq', 1250, true);
-- Próximo nextval() retornará 1251

D. Sincronizar phinxlog

Métodos:

  • detectMissingMigrations() - Detecta migraciones en origen que NO están en destino
  • filterMigrationsForTable() - Filtra solo las que afectan a la tabla consolidada
  • markMigrationsAsExecuted() - Inserta en phinxlog destino (simula --fake)

SQL de Detección:

sql
-- Encontrar migraciones en suc0001 que no están en public
SELECT o.version, o.migration_name, o.start_time, o.end_time
FROM suc0001.phinxlog o
LEFT JOIN public.phinxlog d ON o.version = d.version
WHERE d.version IS NULL
ORDER BY o.version ASC;

Filtrado por Tabla:

php
// Leer archivo de migración: migrations/tenancy/20231201120000_create_ordcon_table.php
// Verificar si extiende ConfigurableMigration
if (preg_match('/class\s+\w+\s+extends\s+ConfigurableMigration/', $content)) {
    // Extraer getTableName()
    preg_match('/protected\s+function\s+getTableName\(\)\s*:\s*string\s*\{\s*return\s+[\'"]([^\'"]+)[\'"]/', $content, $matches);
    $migrationTable = $matches[1];  // 'ordcon'

    if ($migrationTable === 'ordcon') {
        // Esta migración afecta a la tabla consolidada
    }
}

SQL de Sincronización:

sql
-- Marcar migración como ejecutada en public (simula --fake)
INSERT INTO public.phinxlog
  (version, migration_name, start_time, end_time, breakpoint)
VALUES
  (20231201120000, 'CreateOrdconTable', NOW(), NOW(), 0)
ON CONFLICT (version) DO NOTHING;

E. Eliminar Tabla Origen

sql
-- Eliminar tabla en schema hijo (CASCADE elimina dependencias)
DROP TABLE IF EXISTS suc0001.ordcon CASCADE;

Decisión Arquitectónica: No validar Foreign Keys antes de eliminar porque PostgreSQL CASCADE maneja automáticamente las dependencias. Si existen FKs que apuntan a esta tabla, CASCADE las elimina.

Fase 4: Indicador de Re-Ejecución

Objetivo: Notificar al administrador que debe re-ejecutar migraciones para completar el proceso.

Lógica:

  1. Si al menos 1 tabla se consolidó exitosamente:
    • Crear flag en /tmp/bautista_consolidation_executed_{timestamp}.flag
    • Mostrar mensaje en consola con instrucciones

Output de Consola:

============================================================
🔄 RE-EJECUTAR MIGRACIONES REQUERIDO:
👉 php migrations/migrate-db-command.php --migrate
============================================================

Razón: Después de consolidar datos, las migraciones en el schema padre pueden estar "skipped". Re-ejecutarlas asegura que:

  • Estructuras faltantes se completen (índices, constraints)
  • Migraciones futuras se ejecuten sin errores
  • Estado de phinxlog sea consistente

Key Methods

detectConsolidationCandidates()

Firma:

php
protected function detectConsolidationCandidates(): array

Retorna: Array de candidatos ['table', 'from_schema', 'to_schema', 'current_level', 'configured_levels']

Algoritmo:

  1. Obtener schema actual y nivel
  2. Leer configuracion_niveles_tablas (JSONB) desde sistema
  3. Obtener todas las tablas del schema actual (excluyendo sistema)
  4. Para cada tabla:
    • Verificar si tiene configuración personalizada
    • Si nivel actual NO está en configured_levels → candidata
    • Determinar schema padre usando getParentSchemaForConsolidation()
  5. Retornar lista de candidatos

Dependencias:

  • getSystemData() - Lee tabla sistema
  • getPrincipalConnection() - Obtiene schema actual
  • getSchemaLevel() - Determina nivel jerárquico
  • getParentSchemaForConsolidation() - Schema destino

validateTableConsolidation()

Firma:

php
protected function validateTableConsolidation(string $table, string $targetSchema): array

Parámetros:

  • $table - Nombre de la tabla
  • $targetSchema - Schema destino (padre)

Retorna:

php
[
  'can_consolidate' => bool,
  'conflict_reason' => string,
  'schemas_with_data' => array  // ['suc0001' => 100, 'suc0002' => 50]
]

Algoritmo:

  1. Obtener schemas hermanos usando getSiblingSchemas()
  2. Contar registros en cada schema hermano (incluyendo el actual)
  3. Validar:
    • 0 schemas con datos → consolidar (eliminar vacías)
    • 1 schema con datos → consolidar (mover único conjunto)
    • 2+ schemas con datos → CONFLICTO

Dependencias:

  • getSiblingSchemas() - Detecta schemas del mismo nivel
  • countRecordsInSchema() - Cuenta registros

getSiblingSchemas()

Firma:

php
protected function getSiblingSchemas(): array

Retorna: Array de schemas hermanos (sin incluir el actual)

Algoritmo por Nivel:

Nivel 2 (Sucursales):

sql
SELECT schema_name
FROM information_schema.schemata
WHERE schema_name ~ '^suc[0-9]+$'
  AND schema_name != :current_schema
ORDER BY schema_name;

Nivel 3 (Cajas):

php
// Extraer prefijo de sucursal
if (preg_match('/^(suc[0-9]+)caja[0-9]+$/', $currentSchema, $matches)) {
    $sucursalPrefix = $matches[1];  // 'suc0001'

    // Buscar otras cajas de la misma sucursal
    $pattern = '^' . $sucursalPrefix . 'caja[0-9]+$';  // '^suc0001caja[0-9]+$'
}
sql
SELECT schema_name
FROM information_schema.schemata
WHERE schema_name ~ :pattern
  AND schema_name != :current_schema
ORDER BY schema_name;

consolidateTableToParent()

Firma:

php
protected function consolidateTableToParent(string $table, string $sourceSchema, string $targetSchema): bool

Parámetros:

  • $table - Nombre de la tabla
  • $sourceSchema - Schema origen (hijo)
  • $targetSchema - Schema destino (padre)

Retorna: true si éxito, false si error

Algoritmo (dentro de transacción):

  1. Verificar/Crear estructura en destino (LIKE INCLUDING ALL)
  2. Contar registros en origen
  3. Si tiene datos:
    • Copiar usando migrateDataWithExplicitMapping()
    • Actualizar secuencias con updateTableSequences()
  4. Sincronizar phinxlog:
    • Detectar migraciones faltantes con detectMissingMigrations()
    • Filtrar por tabla con filterMigrationsForTable()
    • Marcar como ejecutadas con markMigrationsAsExecuted()
  5. Eliminar tabla origen (DROP CASCADE)
  6. Commit transacción

Manejo de Errores:

  • Rollback automático en caso de excepción
  • Logging con Logger::logDataMigrationError()

filterMigrationsForTable()

Firma:

php
protected function filterMigrationsForTable(array $migrations, string $tableName): array

Parámetros:

  • $migrations - Lista de migraciones faltantes
  • $tableName - Nombre de la tabla consolidada

Retorna: Migraciones filtradas que afectan a la tabla

Algoritmo:

  1. Para cada migración:
    • Construir path del archivo: migrations/tenancy/{version}_{name}.php
    • Leer contenido del archivo
    • Verificar si extiende ConfigurableMigration
    • Extraer getTableName() usando regex
    • Si getTableName() retorna el $tableName buscado → incluir en resultados

Regex para Extracción:

php
// Verificar clase
preg_match('/class\s+\w+\s+extends\s+ConfigurableMigration/', $content)

// Extraer table name
preg_match(
    '/protected\s+function\s+getTableName\(\)\s*:\s*string\s*\{\s*return\s+[\'"]([^\'"]+)[\'"]/',
    $content,
    $matches
);
$migrationTable = $matches[1];  // 'ordcon'

Decisión Arquitectónica: Solo marcar migraciones específicas de la tabla evita sincronizar TODO el phinxlog (lo cual marcaría migraciones de otras tablas que no deberían ejecutarse).

markMigrationsAsExecuted()

Firma:

php
protected function markMigrationsAsExecuted(\PDO $pdo, string $targetSchema, array $migrations): int

Parámetros:

  • $pdo - Conexión PDO
  • $targetSchema - Schema destino (padre)
  • $migrations - Migraciones a marcar

Retorna: Número de migraciones sincronizadas

SQL:

sql
INSERT INTO {$targetSchema}.phinxlog
  (version, migration_name, start_time, end_time, breakpoint)
VALUES
  (:version, :migration_name, NOW(), NOW(), 0)
ON CONFLICT (version) DO NOTHING;

Decisión Arquitectónica: Usar ON CONFLICT DO NOTHING evita errores si la migración ya está marcada (idempotencia).


Critical SQL Queries

1. Detección de Schemas Hermanos (Nivel 2)

sql
-- Buscar todas las sucursales excepto la actual
SELECT schema_name
FROM information_schema.schemata
WHERE schema_name ~ '^suc[0-9]+$'      -- Regex: suc seguido de dígitos
  AND schema_name != 'suc0001'         -- Excluir el actual
ORDER BY schema_name;

Resultado Ejemplo:

schema_name
-----------
suc0002
suc0003
suc0010

2. Detección de Schemas Hermanos (Nivel 3)

sql
-- Buscar todas las cajas de suc0001 excepto la actual
SELECT schema_name
FROM information_schema.schemata
WHERE schema_name ~ '^suc0001caja[0-9]+$'   -- Regex específico de sucursal
  AND schema_name != 'suc0001caja001'       -- Excluir el actual
ORDER BY schema_name;

Resultado Ejemplo:

schema_name
--------------
suc0001caja002
suc0001caja003

3. Comparación de phinxlog entre Schemas

sql
-- Encontrar migraciones en origen (suc0001) que NO están en destino (public)
SELECT
    o.version,
    o.migration_name,
    o.start_time,
    o.end_time
FROM suc0001.phinxlog o
LEFT JOIN public.phinxlog d ON o.version = d.version
WHERE d.version IS NULL
ORDER BY o.version ASC;

Resultado Ejemplo:

version         | migration_name         | start_time          | end_time
----------------|------------------------|---------------------|---------------------
20231201120000  | CreateOrdconTable      | 2023-12-01 12:00:00 | 2023-12-01 12:00:05
20231201130000  | AddOrdconIndexes       | 2023-12-01 13:00:00 | 2023-12-01 13:00:02
20231201140000  | CreateMulctaTable      | 2023-12-01 14:00:00 | 2023-12-01 14:00:03

4. Inserción Selectiva en phinxlog (Simular --fake)

sql
-- Marcar migración como ejecutada sin ejecutarla
INSERT INTO public.phinxlog
  (version, migration_name, start_time, end_time, breakpoint)
VALUES
  (20231201120000, 'CreateOrdconTable', NOW(), NOW(), 0)
ON CONFLICT (version) DO NOTHING;

Notas:

  • ON CONFLICT DO NOTHING - Idempotente
  • breakpoint = 0 - Sin punto de interrupción
  • start_time = end_time = NOW() - Simulación de ejecución instantánea

5. Creación de Estructura con LIKE INCLUDING ALL

sql
-- Copiar estructura completa de tabla (schema + datos + constraints + indexes)
CREATE TABLE public.ordcon (
  LIKE suc0001.ordcon INCLUDING ALL
);

LIKE INCLUDING ALL incluye:

  • INCLUDING DEFAULTS - Valores por defecto de columnas
  • INCLUDING CONSTRAINTS - CHECK constraints, NOT NULL
  • INCLUDING INDEXES - Todos los índices (incluyendo PRIMARY KEY)
  • INCLUDING STORAGE - Configuración de almacenamiento
  • INCLUDING COMMENTS - Comentarios de columnas/tabla
  • INCLUDING IDENTITY - Columnas IDENTITY (serial)
  • INCLUDING GENERATED - Columnas generadas

NO incluye:

  • Datos (se copian después con INSERT SELECT)
  • Foreign Keys (deben recrearse manualmente)
  • Triggers (deben recrearse manualmente)

6. Conteo de Registros en Schema Específico

sql
-- Contar registros en tabla de schema específico
SELECT COUNT(*)
FROM suc0001.ordcon;

Manejo de Errores (en código PHP):

php
try {
    $stmt = $pdo->prepare("SELECT COUNT(*) FROM {$schema}.{$table}");
    $stmt->execute();
    return (int) $stmt->fetchColumn();
} catch (\Exception $e) {
    // Tabla no existe en este schema, retornar 0
    return 0;
}

7. Obtener Columnas de Tabla

sql
-- Obtener todas las columnas en orden de definición
SELECT column_name
FROM information_schema.columns
WHERE table_schema = 'suc0001'
  AND table_name = 'ordcon'
ORDER BY ordinal_position;

Uso: Encontrar columnas comunes entre origen y destino para mapeo explícito.

8. Actualizar Secuencias

sql
-- Paso 1: Obtener valor máximo actual
SELECT COALESCE(MAX(id), 0) FROM public.ordcon;
-- Resultado: 1250

-- Paso 2: Establecer próximo valor de secuencia
SELECT setval('public.ordcon_id_seq', 1250, true);
-- Parámetros:
--   'public.ordcon_id_seq' - Nombre de la secuencia
--   1250 - Último valor usado
--   true - is_called=true (próximo nextval retorna 1251)

Si tabla está vacía:

sql
-- Resetear secuencia a 1
SELECT setval('public.ordcon_id_seq', 1, false);
-- false - is_called=false (próximo nextval retorna 1)

Use Cases & Examples

Caso de Uso 1: Consolidación Exitosa (Tabla con Datos)

Contexto:

  • Schema actual: suc0001 (nivel 2)
  • Tabla: ordcon (clientes)
  • Configuración inicial: {"ordcon": [1, 2]}
  • Configuración nueva: {"ordcon": [1]}
  • Estado: suc0001.ordcon tiene 150 registros

Flujo:

  1. Detección:
🔄 Iniciando consolidación inversa de schemas
📍 Schema actual: suc0001 (Nivel: Sucursal)
📋 1 tabla(s) candidata(s) detectada(s)
  1. Validación:
🔍 Validando 'ordcon'...
   Schemas hermanos encontrados: suc0002, suc0003
   suc0001: 150 registros
   suc0002: 0 registros
   suc0003: 0 registros
✅ 'ordcon': Sin conflictos
  1. Consolidación:
📦 Procesando 'ordcon'...
🔧 Creando estructura en 'public'...
📦 Copiando 150 registros...
✓ 150 registros migrados
🔢 Secuencias actualizadas: 1
📊 Sincronizando historial de migraciones...
📋 Detectadas 3 migraciones faltantes
   ✓ 'CreateOrdconTable' afecta 'ordcon'
   ✓ 'AddOrdconIndexes' afecta 'ordcon'
✓ 2 migraciones sincronizadas para 'ordcon'
🗑️ Eliminando tabla en schema origen...
✅ Consolidada en 1.23s
  1. Indicador:
============================================================
🔄 RE-EJECUTAR MIGRACIONES REQUERIDO:
👉 php migrations/migrate-db-command.php --migrate
============================================================

📊 Resumen: 1 exitosa(s), 0 fallida(s)
🎉 Consolidación completada

Caso de Uso 2: Conflicto (Múltiples Schemas con Datos)

Contexto:

  • Schema actual: suc0001 (nivel 2)
  • Tabla: factura
  • Configuración nueva: {"factura": [1]}
  • Estado:
    • suc0001.factura: 500 registros
    • suc0002.factura: 300 registros

Flujo:

  1. Detección:
🔄 Iniciando consolidación inversa de schemas
📍 Schema actual: suc0001 (Nivel: Sucursal)
📋 1 tabla(s) candidata(s) detectada(s)
  1. Validación:
🔍 Validando 'factura'...
❌ CONFLICTO en 'factura': Múltiples schemas con datos (2 schemas)
📊 Datos en múltiples schemas:
   - suc0001: 500 registros
   - suc0002: 300 registros
  1. Abortar:
🛑 Consolidación abortada por conflictos
⚠️  Se detectaron datos en múltiples schemas para una o más tablas
💡 Solución: Consolidar datos manualmente o ajustar configuración

Solución Manual:

Opción 1 - Consolidar datos de ambas sucursales:

sql
-- Copiar datos de suc0001 a public
INSERT INTO public.factura SELECT * FROM suc0001.factura ON CONFLICT DO NOTHING;

-- Copiar datos de suc0002 a public
INSERT INTO public.factura SELECT * FROM suc0002.factura ON CONFLICT DO NOTHING;

-- Actualizar secuencias
SELECT setval('public.factura_id_seq', (SELECT MAX(id) FROM public.factura), true);

-- Eliminar tablas origen
DROP TABLE suc0001.factura CASCADE;
DROP TABLE suc0002.factura CASCADE;

Opción 2 - Revertir configuración:

json
// Mantener factura en nivel 2
{ "factura": [2] }

Caso de Uso 3: Tablas Vacías (Solo Sincronizar Estructura)

Contexto:

  • Schema actual: suc0001 (nivel 2)
  • Tabla: movimi (movimientos)
  • Configuración nueva: {"movimi": [1]}
  • Estado: suc0001.movimi existe pero sin datos (0 registros)

Flujo:

📦 Procesando 'movimi'...
🔧 Creando estructura en 'public'...
ℹ️  Tabla vacía, solo sincronizando estructura
📊 Sincronizando historial de migraciones...
✓ 1 migraciones sincronizadas para 'movimi'
🗑️ Eliminando tabla en schema origen...
✅ Consolidada en 0.15s

Resultado:

  • public.movimi creada con estructura completa
  • suc0001.movimi eliminada
  • public.phinxlog sincronizado
  • Sin copia de datos (tabla vacía)

Caso de Uso 4: Consolidación de Nivel 3 → 2

Contexto:

  • Schema actual: suc0001caja001 (nivel 3)
  • Tabla: cierre_caja
  • Configuración nueva: {"cierre_caja": [2]}
  • Estado: suc0001caja001.cierre_caja tiene 30 registros

Flujo:

  1. Detección:
🔄 Iniciando consolidación inversa de schemas
📍 Schema actual: suc0001caja001 (Nivel: Caja)
📋 1 tabla(s) candidata(s) detectada(s)
  1. Validación de Hermanos:
🔍 Validando 'cierre_caja'...
   Schemas hermanos encontrados: suc0001caja002, suc0001caja003
   suc0001caja001: 30 registros
   suc0001caja002: 0 registros
   suc0001caja003: 0 registros
✅ 'cierre_caja': Sin conflictos
  1. Consolidación:
📦 Procesando 'cierre_caja'...
🔧 Creando estructura en 'suc0001'...
📦 Copiando 30 registros...
✓ 30 registros migrados
🔢 Secuencias actualizadas: 1
📊 Sincronizando historial de migraciones...
✓ 1 migraciones sincronizadas para 'cierre_caja'
🗑️ Eliminando tabla en schema origen...
✅ Consolidada en 0.45s

Nota: Schema destino es suc0001 (padre inmediato), NO public.


Integration with Migration System

Relación con AASchemaDataMigration

AspectoAASchemaDataMigrationZZSchemaDataConsolidation
DirecciónPadre → HijoHijo → Padre
TriggerMigración inicialCambio de configuración
PrefijoAA (ejecuta primero)ZZ (ejecuta último)
ObjetivoDistribuir datos existentesCentralizar datos dispersos
ValidaciónTabla existe en padreConflictos entre hermanos

Flujo de Despliegue Completo

mermaid
sequenceDiagram
    participant Admin as Administrador
    participant DB as Base de Datos
    participant M1 as Migraciones (Phase 1)
    participant AA as AASchemaDataMigration
    participant ZZ as ZZSchemaDataConsolidation
    participant M2 as Migraciones (Phase 2)

    Admin->>DB: Cambiar configuracion_niveles_tablas
    Note over DB: ordcon: [1,2] → [1]

    Admin->>M1: php migrate-db-command.php --migrate
    M1->>DB: Ejecutar migraciones pendientes
    M1->>DB: Crear estructuras nuevas
    Note over M1: Seed AA ejecuta automáticamente

    M1->>AA: run()
    AA->>DB: Detectar tablas en parent no en child
    AA->>DB: Migrar datos parent → child (si aplica)
    AA-->>M1: Completado

    Note over M1: Seed ZZ ejecuta automáticamente
    M1->>ZZ: run()
    ZZ->>DB: Detectar candidatos (ordcon en suc0001)
    ZZ->>DB: Validar sin conflictos
    ZZ->>DB: Consolidar suc0001.ordcon → public.ordcon
    ZZ->>DB: Sincronizar phinxlog
    ZZ->>ZZ: Crear flag /tmp/bautista_consolidation_*.flag
    ZZ-->>Admin: ⚠️ RE-EJECUTAR MIGRACIONES REQUERIDO

    Admin->>M2: php migrate-db-command.php --migrate
    M2->>DB: Detectar phinxlog sincronizado
    M2->>DB: Ejecutar migraciones pendientes
    M2->>DB: Completar estructuras faltantes
    M2-->>Admin: ✅ Proceso completado

Flag de Re-Ejecución

Path: /tmp/bautista_consolidation_executed_{timestamp}.flag

Propósito:

  • Indicador persistente de que se requiere re-ejecutar migraciones
  • Timestamp permite rastrear cuándo ocurrió la consolidación
  • Puede ser usado por scripts de CI/CD para detectar necesidad de re-ejecución

Ejemplo:

bash
/tmp/bautista_consolidation_executed_1738440123.flag

Script de Verificación:

bash
#!/bin/bash
if ls /tmp/bautista_consolidation_*.flag 1> /dev/null 2>&1; then
    echo "⚠️  Consolidación detectada, re-ejecutando migraciones..."
    php migrations/migrate-db-command.php --migrate
    rm /tmp/bautista_consolidation_*.flag
else
    echo "✅ Sin consolidaciones pendientes"
fi

Sincronización de phinxlog

Problema: Cuando AASchemaDataMigration detecta que una tabla ya existe en un schema hijo (por ejemplo, creada por una migración anterior), omite crearla nuevamente. Sin embargo, esto puede dejar el phinxlog del schema padre sin esas migraciones marcadas, causando errores futuros.

Solución de ZZ:

  1. Detecta migraciones en hijo que NO están en padre
  2. Filtra solo las relevantes a la tabla consolidada
  3. Las marca como ejecutadas en el padre (simula --fake)

Ejemplo:

Estado inicial:
  suc0001.phinxlog: [20231201120000_CreateOrdconTable, 20231201130000_AddOrdconIndexes]
  public.phinxlog: []

Después de consolidación:
  public.phinxlog: [20231201120000_CreateOrdconTable, 20231201130000_AddOrdconIndexes]

Beneficio: Al re-ejecutar migraciones, Phinx ve que CreateOrdconTable ya está marcada en public.phinxlog, entonces la omite en lugar de fallar con "table already exists".


Error Handling

Validación de Conflictos

Detección:

php
if ($schemasWithDataCount > 1) {
    return [
        'can_consolidate' => false,
        'conflict_reason' => "Múltiples schemas con datos ({$schemasWithDataCount} schemas)",
        'schemas_with_data' => $schemasWithData
    ];
}

Output:

❌ CONFLICTO en 'ordcon': Múltiples schemas con datos (2 schemas)
📊 Datos en múltiples schemas:
   - suc0001: 150 registros
   - suc0002: 100 registros

🛑 Consolidación abortada por conflictos
⚠️  Se detectaron datos en múltiples schemas para una o más tablas
💡 Solución: Consolidar datos manualmente o ajustar configuración

Decisión Arquitectónica: Abortar COMPLETAMENTE (no consolidar ninguna tabla) si se detecta al menos un conflicto. Esto evita estados inconsistentes donde algunas tablas se consolidan y otras no.

Transacciones Independientes por Tabla

Implementación:

php
foreach ($validated as $candidate) {
    try {
        $pdo->beginTransaction();

        // A. Crear estructura
        // B. Copiar datos
        // C. Actualizar secuencias
        // D. Sincronizar phinxlog
        // E. Eliminar origen

        $pdo->commit();
        $results['success'][] = $candidate['table'];

    } catch (\Exception $e) {
        $pdo->rollBack();
        $results['failed'][] = $candidate['table'];
        $this->output->writeln("❌ Error consolidando '{$candidate['table']}'");
    }
}

Beneficio: Una tabla fallida NO bloquea la consolidación de otras tablas. El proceso continúa y reporta éxitos/fallos al final.

Ejemplo de Fallo Parcial:

📦 Procesando 'ordcon'...
✅ 'ordcon' consolidada exitosamente

📦 Procesando 'factura'...
❌ Error consolidando 'factura'

📊 Resumen: 1 exitosa(s), 1 fallida(s)

Logging con Logger Class

Ejemplos de Logging:

php
// Inicio de consolidación
\App\Logger::logMigrationCandidates($candidates, 'Configuration change detected');

// Validación
\App\Logger::logDataValidation(
    $candidate['table'],
    $currentSchema,
    false,
    $validation
);

// Migración exitosa
\App\Logger::logDataMigrationEnd($table, $sourceSchema, $targetSchema, $recordCount, $duration);

// Error
\App\Logger::logDataMigrationError($table, $sourceSchema, $targetSchema, $e->getMessage());

Archivo de Log: migrations/logs/migrations.log

Formato:

[2026-02-11 10:15:23] migrations.INFO: Candidatos de migración detectados {"candidates":[{"table":"ordcon","from_schema":"suc0001","to_schema":"public"}],"candidate_count":1,"type":"migration_detection","action":"candidates_detected","reason":"Configuration change detected"}

[2026-02-11 10:15:25] migrations.INFO: Finalizando migración de datos: ordcon {"table":"ordcon","source_schema":"suc0001","target_schema":"public","migrated_count":150,"type":"data_migration","action":"end","duration":"1.23s"}

Rollback Automático en Errores

Escenarios de Rollback:

  1. Error al crear estructura:
php
try {
    $sql = "CREATE TABLE {$targetSchema}.{$table} (LIKE {$sourceSchema}.{$table} INCLUDING ALL)";
    $pdo->exec($sql);
} catch (\Exception $e) {
    throw new \Exception("No se pudo crear la estructura en schema destino");
}
// Rollback automático si falla
  1. Error al copiar datos:
php
// Dentro de transacción
$migratedCount = $this->migrateDataWithExplicitMapping($pdo, $table, $sourceSchema, $targetSchema);
// Si falla, rollback automático
  1. Error al sincronizar phinxlog:
php
$synced = $this->markMigrationsAsExecuted($pdo, $targetSchema, $relevantMigrations);
// Si falla, rollback automático (estructura y datos NO se persisten)

Garantía: Todas las operaciones dentro de consolidateTableToParent() están en una transacción, asegurando atomicidad.

Manejo de Tablas Inexistentes

Escenario: Intentar contar registros en un schema donde la tabla no existe.

Implementación:

php
protected function countRecordsInSchema(string $tableName, string $schemaName): int
{
    $pdo = $this->getPdoConnection(true);

    try {
        $stmt = $pdo->prepare("SELECT COUNT(*) FROM {$schemaName}.{$tableName}");
        $stmt->execute();
        return (int) $stmt->fetchColumn();
    } catch (Exception) {
        return 0;  // Tabla no existe, retornar 0
    }
}

Beneficio: El proceso de validación continúa incluso si algunos schemas hermanos no tienen la tabla.


Architectural Decisions

1. Por qué prefijo ZZ (ejecuta al final)

Razón: Phinx ejecuta seeds en orden alfabético. El prefijo ZZ asegura que ZZSchemaDataConsolidation ejecute DESPUÉS de AASchemaDataMigration.

Flujo:

AASchemaDataMigration    (padre → hijo)

BBOtroSeedSiExiste

...

ZZSchemaDataConsolidation (hijo → padre)

Importancia: AA debe ejecutar primero para distribuir datos existentes ANTES de que ZZ consolide datos inversos. Si ZZ ejecutara primero, podría consolidar datos que luego AA intentaría distribuir nuevamente, causando inconsistencias.

2. Por qué filtrar migraciones por tabla

Alternativa rechazada: Marcar TODAS las migraciones faltantes en phinxlog destino.

Problema:

Migraciones en suc0001 pero no en public:
  - 20231201120000_CreateOrdconTable  ← Afecta ordcon (consolidada)
  - 20231201130000_CreateFacturaTable ← Afecta factura (NO consolidada)
  - 20231201140000_CreateMovimiTable  ← Afecta movimi (NO consolidada)

Si marcamos TODAS: factura y movimi quedan en estado "ejecutada" en public pero no existen.
Resultado: Migraciones futuras fallan o se omiten incorrectamente.

Solución: Filtrar usando filterMigrationsForTable() para marcar SOLO las migraciones que afectan a ordcon.

Implementación:

php
$relevantMigrations = $this->filterMigrationsForTable($missingMigrations, 'ordcon');
// Resultado: Solo [20231201120000_CreateOrdconTable]

3. Por qué NO validar Foreign Keys antes de eliminar

Alternativa rechazada: Consultar information_schema.table_constraints para verificar FKs antes de DROP TABLE.

Decisión: Usar DROP TABLE ... CASCADE sin validación previa.

Razones:

  1. PostgreSQL CASCADE maneja automáticamente:
sql
DROP TABLE suc0001.ordcon CASCADE;
-- Elimina automáticamente:
--   - Foreign keys en otras tablas que apuntan a ordcon
--   - Vistas que referencian ordcon
--   - Triggers en ordcon
  1. Validación es innecesaria:

    • Si existen FKs, CASCADE las elimina sin error
    • Si NO existen FKs, CASCADE es no-op (sin efecto)
    • Validar manualmente agrega complejidad sin beneficio
  2. Performance:

    • Evita queries adicionales a information_schema
    • Simplifica código

Riesgo mitigado: Después de consolidar, re-ejecutar migraciones recrea las FKs necesarias en el schema destino.

4. Por qué transacciones independientes por tabla

Alternativa rechazada: Una sola transacción para todas las tablas.

Problema con transacción única:

php
$pdo->beginTransaction();

consolidateTable('ordcon');   // ✅ Éxito
consolidateTable('factura');  // ❌ Error
consolidateTable('movimi');   // Nunca se ejecuta

$pdo->rollBack();  // Rollback TODO (ordcon también)

Resultado: Una tabla fallida bloquea todas las demás.

Solución con transacciones independientes:

php
foreach ($tables as $table) {
    try {
        $pdo->beginTransaction();
        consolidateTable($table);
        $pdo->commit();
        $success[] = $table;
    } catch (\Exception $e) {
        $pdo->rollBack();
        $failed[] = $table;
    }
}

Resultado: ordcon y movimi se consolidan exitosamente; solo factura falla.

Beneficio: Maximiza el progreso incluso con errores parciales.

5. Por qué usar LIKE INCLUDING ALL

Alternativa rechazada: Copiar estructura manualmente (crear columnas, constraints, indexes individualmente).

Decisión: Usar CREATE TABLE ... (LIKE source INCLUDING ALL).

Ventajas:

AspectoLIKE INCLUDING ALLManual
Simplicidad1 línea SQL50+ líneas
CompletitudCopia TODO (defaults, constraints, indexes)Propenso a omisiones
MantenimientoAuto-adapta a cambios en estructuraRequiere actualizar código
PerformancePostgreSQL optimizadoMúltiples queries

Limitaciones conocidas:

  • NO copia Foreign Keys (se recrean con re-ejecución de migraciones)
  • NO copia Triggers (se recrean con re-ejecución de migraciones)
  • NO copia datos (se copian después con INSERT SELECT)

Mitigación: Re-ejecución de migraciones después de consolidación completa FKs y Triggers faltantes.

6. Por qué contar registros en schemas hermanos

Alternativa rechazada: Solo validar schema actual vs. schema padre.

Problema:

Escenario:
  suc0001.ordcon: 100 registros
  suc0002.ordcon: 50 registros
  public.ordcon: No existe

Sin validar hermanos:
  ✅ Consolidar suc0001 → public (100 registros)
  Próxima ejecución en suc0002:
  ✅ Consolidar suc0002 → public (50 registros)

Resultado: Pérdida de 100 registros de suc0001 (sobrescritos por suc0002)

Solución: Validar TODOS los schemas hermanos ANTES de consolidar.

Implementación:

php
$siblingSchemas = $this->getSiblingSchemas();  // [suc0002, suc0003, ...]
foreach ($siblingSchemas as $schema) {
    $count = $this->countRecordsInSchema($table, $schema);
    if ($count > 0) {
        $schemasWithData[$schema] = $count;
    }
}

if (count($schemasWithData) > 1) {
    // CONFLICTO: Múltiples schemas con datos
}

Beneficio: Evita pérdida de datos al detectar conflictos ANTES de consolidar.

7. Por qué crear flag de re-ejecución

Alternativa rechazada: Re-ejecutar migraciones automáticamente dentro del seed.

Problemas:

  1. Recursión infinita:
ZZConsolidation → migrate-db-command → ZZConsolidation → ...
  1. Complejidad de contexto:

    • Seeds no tienen acceso directo al sistema de migraciones Phinx
    • Ejecutar php migrate-db-command.php desde dentro de un seed es anti-patrón
  2. Control del administrador:

    • Admin pierde visibilidad de qué ocurre
    • Dificulta troubleshooting

Solución: Crear flag + mostrar instrucciones claras.

Beneficios:

BeneficioDescripción
TransparenciaAdmin sabe exactamente qué hacer
ControlAdmin decide cuándo re-ejecutar
TrazabilidadFlag permite auditar cuándo ocurrió consolidación
CI/CDScripts automatizados pueden detectar flag

Ejemplo de Script CI/CD:

bash
# En pipeline de despliegue
php migrations/migrate-db-command.php --migrate
php migrations/seed-db-command.php --run

if ls /tmp/bautista_consolidation_*.flag 1> /dev/null 2>&1; then
    echo "⚠️  Consolidación detectada, re-ejecutando migraciones..."
    php migrations/migrate-db-command.php --migrate
    rm /tmp/bautista_consolidation_*.flag
fi

Maintenance & Operations

Monitoreo de Consolidaciones

Archivo de log: migrations/logs/migrations.log

Queries útiles para análisis:

bash
# Buscar consolidaciones en logs
grep "Candidatos de migración detectados" migrations/logs/migrations.log

# Buscar conflictos detectados
grep "CONFLICTO" migrations/logs/migrations.log

# Buscar consolidaciones exitosas
grep "Finalizando migración de datos" migrations/logs/migrations.log | grep "type.*data_migration"

# Buscar errores en consolidación
grep "Error en migración de datos" migrations/logs/migrations.log

Métricas clave:

MétricaQuery
Total consolidacionesgrep -c "Candidatos de migración detectados" migrations.log
Conflictos detectadosgrep -c "CONFLICTO" migrations.log
Tiempo promedioParsear "duration" de logs

Troubleshooting

Problema 1: Consolidación abortada por conflictos

Síntoma:

🛑 Consolidación abortada por conflictos
⚠️  Se detectaron datos en múltiples schemas para una o más tablas

Diagnóstico:

sql
-- Verificar distribución de datos
SELECT 'suc0001' AS schema, COUNT(*) FROM suc0001.ordcon
UNION ALL
SELECT 'suc0002' AS schema, COUNT(*) FROM suc0002.ordcon
UNION ALL
SELECT 'suc0003' AS schema, COUNT(*) FROM suc0003.ordcon;

Soluciones:

  1. Consolidación manual de datos:
sql
-- Copiar de todos los schemas
INSERT INTO public.ordcon SELECT * FROM suc0001.ordcon ON CONFLICT DO NOTHING;
INSERT INTO public.ordcon SELECT * FROM suc0002.ordcon ON CONFLICT DO NOTHING;
INSERT INTO public.ordcon SELECT * FROM suc0003.ordcon ON CONFLICT DO NOTHING;

-- Actualizar secuencias
SELECT setval('public.ordcon_id_seq', (SELECT MAX(id) FROM public.ordcon), true);

-- Eliminar orígenes
DROP TABLE suc0001.ordcon CASCADE;
DROP TABLE suc0002.ordcon CASCADE;
DROP TABLE suc0003.ordcon CASCADE;
  1. Revertir configuración (si datos deben permanecer distribuidos):
sql
UPDATE sistema
SET configuracion_niveles_tablas = jsonb_set(
  COALESCE(configuracion_niveles_tablas, '{}'::jsonb),
  '{ordcon}',
  '[2]'::jsonb
)
WHERE bd = 'nombre_bd';

Problema 2: Re-ejecución de migraciones falla

Síntoma:

php migrations/migrate-db-command.php --migrate
Error: Table 'ordcon' already exists

Diagnóstico:

sql
-- Verificar estado de phinxlog
SELECT version, migration_name
FROM public.phinxlog
WHERE migration_name LIKE '%ordcon%';

Causa: Migración no se sincronizó correctamente en phinxlog.

Solución:

sql
-- Marcar migración como ejecutada manualmente
INSERT INTO public.phinxlog
  (version, migration_name, start_time, end_time, breakpoint)
VALUES
  (20231201120000, 'CreateOrdconTable', NOW(), NOW(), 0)
ON CONFLICT (version) DO NOTHING;

Problema 3: Secuencias desincronizadas

Síntoma:

ERROR:  duplicate key value violates unique constraint "ordcon_pkey"
DETAIL:  Key (id)=(150) already exists.

Diagnóstico:

sql
-- Ver valor actual de secuencia
SELECT last_value FROM public.ordcon_id_seq;

-- Ver máximo ID en tabla
SELECT MAX(id) FROM public.ordcon;

Solución:

sql
-- Sincronizar secuencia manualmente
SELECT setval('public.ordcon_id_seq', (SELECT MAX(id) FROM public.ordcon), true);

Mejores Prácticas

1. Backup antes de cambiar configuración

bash
# Backup de configuración actual
pg_dump -h localhost -U postgres -d sistema_bd \
  -t sistema \
  --data-only \
  -f backup_configuracion_niveles_$(date +%Y%m%d_%H%M%S).sql

# Backup de datos completo (opcional)
pg_dump -h localhost -U postgres -d sistema_bd \
  -f backup_full_$(date +%Y%m%d_%H%M%S).sql

2. Validar configuración antes de aplicar

sql
-- Ver configuración actual
SELECT bd, configuracion_niveles_tablas
FROM sistema
WHERE bd = 'nombre_bd';

-- Validar JSON antes de actualizar
SELECT jsonb_pretty(configuracion_niveles_tablas)
FROM sistema
WHERE bd = 'nombre_bd';

3. Ejecutar en ambiente de prueba primero

bash
# 1. Cambiar configuración en BD prueba
UPDATE sistema
SET configuracion_niveles_tablas = '{"ordcon": [1]}'::jsonb
WHERE bd = 'test_bd';

# 2. Ejecutar migraciones en prueba
php migrations/migrate-db-command.php --migrate --database=test_bd

# 3. Verificar resultados
psql -U postgres -d test_bd -c "SELECT * FROM public.ordcon LIMIT 5;"

# 4. Si éxito, aplicar en producción

4. Monitorear logs durante consolidación

bash
# Terminal 1: Ejecutar consolidación
php migrations/seed-db-command.php --run --verbosity

# Terminal 2: Monitorear logs en tiempo real
tail -f migrations/logs/migrations.log | grep -E "(CONFLICTO|ERROR|consolidación)"

Comandos Útiles

bash
# Ver candidatos de consolidación (dry-run simulado)
psql -U postgres -d sistema_bd -c "
SELECT
    t.table_name,
    t.table_schema,
    s.configuracion_niveles_tablas->t.table_name AS configured_levels
FROM information_schema.tables t
CROSS JOIN sistema s
WHERE t.table_schema = 'suc0001'
  AND t.table_type = 'BASE TABLE'
  AND s.configuracion_niveles_tablas ? t.table_name
  AND NOT (s.configuracion_niveles_tablas->t.table_name @> '2'::jsonb);
"

# Limpiar flags antiguos
find /tmp -name "bautista_consolidation_*.flag" -mtime +7 -delete

# Verificar schemas hermanos
psql -U postgres -d sistema_bd -c "
SELECT schema_name
FROM information_schema.schemata
WHERE schema_name ~ '^suc[0-9]+$'
ORDER BY schema_name;
"

# Ver estado de phinxlog entre schemas
psql -U postgres -d sistema_bd -c "
SELECT
    'suc0001' AS schema,
    COUNT(*) AS migrations
FROM suc0001.phinxlog
UNION ALL
SELECT
    'public' AS schema,
    COUNT(*) AS migrations
FROM public.phinxlog;
"

Conceptual Documentation

  • docs/architecture/database/index.md - Overview de patrones de base de datos
  • docs/architecture/database/multi-tenant.md - Multi-tenancy con schemas (ConnectionManager, X-Schema)
  • docs/architecture/database/multi-schema.md - Queries cross-schema (UNION ALL, consolidación)

Technical Implementation

  • bautista-backend/migrations/CLAUDE.md - Sistema de migraciones Phinx, ConfigurableMigration
  • bautista-backend/migrations/seeds/tenancy/AASchemaDataMigration.php - Seed complementario (padre → hijo)
  • bautista-backend/migrations/MigrationTrait.php - Métodos compartidos para multi-tenancy
  • bautista-backend/migrations/Logger.php - Sistema de logging

Management Tools

  • bautista-backend/migrations/manage-table-levels.php - Script para administrar configuracion_niveles_tablas

Version History

VersiónFechaCambios
v3.13.12026-02-11Implementación inicial de ZZSchemaDataConsolidation

Authors & Maintainers

  • Sistema Bautista Development Team
  • Archivo: /bautista-backend/migrations/seeds/tenancy/ZZSchemaDataConsolidation.php
  • Documentación creada: 2026-02-11