GATE del rebuild-por-import: los triggers NO hay que deshabilitarlos (TRUNCATE no dispara ROW triggers), validadoras_historial esta vacia, y el gate de re-habilitacion + fidelidad
Directiva de Elazar 2026-09-03: el import RECONSTRUYE vehiculos, vehiculos_datos_colectivos y vehiculo_historial en cada corrida en vez de mergear. db03 ownea la implementacion dentro de 155/190; este item es el GATE. === HALLAZGO 1: NO HACE FALTA DESHABILITAR NINGUN TRIGGER. Medido, no supuesto. === Los 7 triggers no-internos de las tres tablas son TODOS ROW-level y ninguno es de TRUNCATE: vehiculos: trg_soft_delete_vehiculos (ROW BEFORE DELETE), trg_archive_vehiculos (ROW BEFORE DELETE,UPDATE), trg_cascade_soft_delete_vehiculo (ROW AFTER UPDATE), trg_vehiculos_dominio_norm (ROW BEFORE INSERT,UPDATE) vehiculos_datos_colectivos: trg_soft_delete_* (ROW BEFORE DELETE), trg_archive_* (ROW BEFORE DELETE,UPDATE) vehiculo_historial: trg_soft_delete_vehiculo_historial (ROW BEFORE DELETE) TRUNCATE NO dispara triggers ROW-level. Si el rebuild usa TRUNCATE, el paso de deshabilitar no es necesario y la clase de riesgo 'quedaron apagados en produccion' desaparece por construccion, no por mitigacion. Y DESHABILITAR EN BLOQUE ROMPERIA LA CARGA: trg_vehiculos_dominio_norm es BEFORE INSERT y normaliza el dominio. Apagarlo mientras se re-insertan 2398 vehiculos mete dominios sin normalizar. El unico trigger que corre durante la recarga es justamente el que hay que dejar PRENDIDO. SI SE USA DELETE EN VEZ DE TRUNCATE: usar SET session_replication_role = replica DENTRO de la transaccion. Es de SESION y no puede sobrevivir a la desconexion. NUNCA ALTER TABLE ... DISABLE TRIGGER, que es estado persistente del cluster y es exactamente lo que queda encendido al reves si algo aborta. === HALLAZGO 2: validadoras_historial esta VACIA en PROD. 0 filas. === Su FK a vehiculos es ON DELETE RESTRICT (confdeltype='r'), no CASCADE: si tuviera filas BLOQUEARIA el borrado en vez de arrastrarlo. Como esta vacia, la decision explicita que pedia coder02 no tiene costo hoy: se incluye en el TRUNCATE (obligatorio, porque RESTRICT hace fallar un TRUNCATE que no la nombre) y no se pierde nada. RE-MEDIR el conteo el dia de la corrida: si dejo de ser 0, esta decision se re-abre porque esa tabla NO se puede reconstruir desde el libro. Solo esas tres tablas tienen FK a vehiculos. Confirmado independientemente. === HALLAZGO 3: el patron de procedencia que falta YA EXISTE en este esquema. === Ninguna de las tres tablas tiene columna de actor: cero columnas que matcheen actor|usuario|user|persona|editad|modific. validadoras_historial SI tiene actor_id, actor_nombre y actor_email. O sea la solucion al bloqueo de go-live no es inventar nada, es aplicar a las tablas de vehiculos el patron que las tablas de validadoras ya usan. Y validadoras_historial, validadoras y validadoras_historial_ubicaciones ya tienen triggers *_append_only y *_no_truncate. El patron de proteccion tambien existe. CORRECCION MENOR al conteo de coder02: no es '4224 importador/migracion y 1 de la app'. creado_por en PROD: import-rutap-2026-08-14-prod 2274, system_import 1599, snapshot5 171, import-movilba-19 164, movilba-104-prod 7, import-movilba-18 4, import-movilba-24 3, 'Elazar (GO 2026-08-25 via db02-mba-l-ca)' 2, '20394678369' 1. Son 4222 de importador/migracion, 2 escritas por un agente con GO y 1 sola de la app. La conclusion no cambia y el bloqueo de procedencia tampoco: creado_por no discrimina (MOVILBA-182). === EL GATE, que es lo que ownea db02 === G1 TRIGGERS - LINEA BASE MEDIDA HOY EN PROD: 50 triggers no-internos en public, los 50 con tgenabled='O', CERO deshabilitados. El gate falla si despues del import ese conteo no es identico. Es una consulta, no una revision: SELECT count(*) FILTER (WHERE tgenabled<>'O') FROM pg_trigger t JOIN pg_class c ON c.oid=t.tgrelid JOIN pg_namespace n ON n.oid=c.relnamespace WHERE NOT t.tgisinternal AND n.nspname='public' -- tiene que dar 0, y el total tiene que dar 50. G2 BAJAS - una baja es un ESTADO, no un borrado. Despues del rebuild los dominios que el libro trae con todos sus periodos cerrados tienen que existir, con su BAJA. Gate: cero dominios del libro ausentes de vehiculos, y el conteo de fichas en estado 117 no puede caer. Si el rebuild leyera solo filas abiertas, 798 dominios perderian su historia. G3 FIDELIDAD - los 32 casos de MOVILBA-213 tienen que dar cero despues del import; lo que sobreviva es defecto real. G4 COMPLETITUD - las 170 filas de MOVILBA-216 no pueden reaparecer con linea_id NULL. G5 CONTROL POSITIVO - el gate tiene que fallar si la tabla queda vacia o si la comparacion no tiene entradas. Sin esto G2/G3 pasan por ausencia de input. BLOQUEO DE GO-LIVE, separado de la implementacion: mientras no exista una columna que distinga 'lo escribio una persona' de 'lo escribio el import', el primer dato que un usuario edite es el primero que el proximo import borra en silencio. Hoy sale gratis porque 1 fila de 4225 es de la app. El 09-07 no. Es MOVILBA-182 + el patron actor_* de validadoras. NO EJERCITADO: no medi DEV; no escribi ni propuse ejecutar nada; no revise el codigo del importer.
Activity
-
parent=#3153
-
AMPLIACION DEL ALCANCE (db02, 2026-09-03, SELECT en PROD y DEV). La clasificacion de dependencias por FK deja afuera lo que mas duele, y hay una recomendacion que convierte un riesgo silencioso en uno ruidoso gratis. === 1. NUNCA 'TRUNCATE ... RESTART IDENTITY'. Es la decision mas barata de todas. === vehiculos.id es IDENTITY en los dos entornos. PROD: secuencia public.vehiculos_id_seq last_value=6991, max(id)=6991. DEV: 6623/6623. TRUNCATE SIN RESTART IDENTITY deja la secuencia donde esta: los vehiculos reconstruidos arrancan en 6992 y NINGUN id se reutiliza. Entonces un vehiculo_id viejo que sobreviva en algun lado apunta a una fila QUE NO EXISTE - detectable con un anti-join. TRUNCATE CON RESTART IDENTITY reinicia en 1 y los ids SI se reutilizan, con otro vehiculo adentro. Ahi el puntero viejo apunta a un vehiculo EQUIVOCADO, no da error, y no hay consulta que lo distinga de un puntero correcto. Es el mismo comando con una palabra de diferencia y decide si el defecto es ruidoso o invisible. === 2. LAS TABLAS *_archive NO ESTAN EN LA LISTA DE DEPENDENCIAS Y NO SALEN DEL LIBRO === public.vehiculos_archive: 697 filas en PROD, 25632 en DEV. public.vehiculos_datos_colectivos_archive: 1278 en PROD, 25012 en DEV. Las escriben trg_archive_vehiculos y trg_archive_vehiculos_datos_colectivos (ROW BEFORE DELETE,UPDATE). Guardan vehiculo_id SIN FK, o sea el rebuild las huerfaniza SIN NINGUN ERROR - no hay constraint que se queje. Y son justo la clase que el bloqueo de procedencia protege: son el rastro de QUE CAMBIO, que no se puede reconstruir desde el libro. No estaban en la clasificacion de coder02 ni en la mia. ADEMAS, ARGUMENTO EXTRA PARA TRUNCATE: con DELETE los triggers de archive disparan y cada corrida agrega ~4800 filas de basura a esas tablas. Con TRUNCATE no disparan y no se ensucian. DECISION QUE HACE FALTA: se parkean y se remapean por dominio como deudas_dominios, o se aceptan huerfanas? No la tomo yo. === 3. CORRECCION: rutap.m95_cambios_backfill_dev NO tiene vehiculo_id === Sus columnas son vehiculo_historial_id, cambios_escritos, aplicado_en. No tiene FK a vehiculos y no puede tenerlo. Sus 3787 filas dependen del id sustituto de vehiculo_historial, no del de vehiculos. Misma clase de problema, padre distinto - y el rebuild de vehiculo_historial le reasigna los ids igual. Que se dropee y se reconstruya sigue siendo la respuesta correcta; lo que cambia es que no se llega a el por la lista de FK. Los FK entrantes a public.vehiculos medidos en TODOS los esquemas son 3 en PROD y 5 en DEV (no 6): rendiciones.deudas_dominios, rutap.vehiculo_historial_normalizado, validadoras_historial, vehiculo_historial, vehiculos_datos_colectivos. === 4. validadoras_historial: los dos teniamos razon, sobre entornos distintos === PROD 0 filas. DEV 2 filas, las dos con vehiculo_id NULL. Ninguna decision cambia, pero que quede escrito cual numero es de cual entorno. === 5. LOS DOS GATES QUE ME ASIGNARON, escritos para que fallen fuerte === GATE A - triggers. Linea base PROD medida hoy: 50 triggers no-internos en public, los 50 con tgenabled='O', cero deshabilitados. Post-rebuild tiene que dar EXACTAMENTE 50 y 50. Falla ruidosa si no. trg_vehiculos_dominio_norm no es un trigger de borrado y queda encendido todo el tiempo; un 'disable all' se lo lleva puesto y mete dominios sin normalizar. GATE B - remapeo. Todo dominio parkeado tiene que resolver a un vehiculo reconstruido. La consulta es un anti-join del parking contra vehiculos por dominio y tiene que dar CERO filas. Un conteo de filas NO sirve: un remapeo que pierde 5 filas y gana 5 pasa el conteo. CONTROL POSITIVO OBLIGATORIO en los dos: el gate B tiene que fallar si el parking esta vacio o si la tabla de vehiculos esta vacia, o pasa por falta de entradas y no prueba nada. GATE B EXTENDIDO, si se decide parkear las archive: mismo anti-join por dominio sobre vehiculos_archive y vehiculos_datos_colectivos_archive. NO EJERCITADO: no medi si las filas de las tablas archive tienen dominio recuperable (si no lo tienen, no se pueden remapear por dominio y la decision es otra). Cero escrituras.
-
SEGUIMIENTO, cerrando el 'no ejercitado' que deje arriba (db02, 2026-09-03, PROD). Dos cosas, y la segunda es una respuesta al bloqueo de procedencia. === A. LAS DOS TABLAS ARCHIVE NO SE PARKEAN IGUAL, Y UNA TIENE UNA RESTRICCION DE ORDEN DURA === vehiculos_archive TIENE columna dominio. Se parkea y se remapea por dominio como deudas_dominios, sin dependencias. vehiculos_datos_colectivos_archive NO TIENE dominio. Su unica llave al vehiculo es vehiculo_id, sin FK. CONSECUENCIA: sus 1278 filas de PROD (25012 en DEV) solo se pueden resolver a dominio MIENTRAS vehiculos siga en pie. Hoy resuelven las 1278 de 1278 contra un vehiculo vivo. Despues del TRUNCATE no resuelve NINGUNA y no hay forma de recuperarlo, porque el dominio no esta en la fila. POR LO TANTO: capturar el mapa vehiculo_id -> dominio ANTES del delete es OBLIGATORIO, no opcional, y no se puede posponer ni un paso. Si se trunca primero, esas filas quedan huerfanas para siempre y en silencio - no hay FK que avise. === B. EL DISCRIMINADOR DE PROCEDENCIA YA EXISTE Y ES CONFIABLE: archived_by === vehiculos_datos_colectivos_archive.archived_by en PROD: 1271 NULL, 4 'import-2026-08-05', 2 'svc_nextjs', 1 'import-2026-06-29'. svc_nextjs es el ROL DE BASE con el que se conecta la app. O sea esas 2 filas estan marcadas como escritas POR LA APP, y las otras como import, en una columna donde creado_por no distingue nada (MOVILBA-182). Y LA DIFERENCIA DE MECANISMO ES LA CLAVE, no la columna: archived_by lo pone el trigger desde el rol de la conexion, no lo pasa el que escribe. creado_por es una cadena que el llamador elige, y por eso el import pudo poner 'system_import' en 24 de los 29 vehiculos que la app creo por pantalla. Un valor que el escritor elige no es evidencia de quien escribio. RECOMENDACION PARA 182 Y PARA EL BLOQUEO DE GO-LIVE: poblar la procedencia desde el ROL de la sesion en un trigger (current_user), nunca desde un parametro del llamador. No hay que inventar el mecanismo: esta a la vista en las tablas archive y en actor_id/actor_nombre/actor_email de validadoras_historial. Lo que falta es aplicarlo a las tablas de vehiculos. Los 1271 NULL son de antes de que la columna se poblara; no invalidan el mecanismo, marcan desde cuando sirve. GATE B EXTENDIDO, version final: - parkear vehiculos_archive por dominio (columna propia) - parkear vehiculos_datos_colectivos_archive resolviendo vehiculo_id -> dominio ANTES del delete, y guardar el dominio en el parking - post-rebuild, anti-join por dominio contra vehiculos: CERO filas sin resolver, en las dos - control positivo: el gate falla si el parking esta vacio o si vehiculos esta vacia
-
GATE B, ESPECIFICACION FINAL DE PARKING. db03 encontro que rendiciones.deudas_dominios.dominio no sirve como llave; lo verifique y ademas corri EL MISMO TEST sobre la columna que yo habia recomendado usar, porque mi recomendacion tenia la misma forma que su hallazgo. EL TEST, generico: una columna dominio DESNORMALIZADA no es confiable por existir. Se prueba comparandola contra el dominio VIVO al que llega por su vehiculo_id. rendiciones.deudas_dominios (DEV, 233 filas): dominio propio VACIO en 231, coincide con vehiculos.dominio en 0, difiere no-vacio en 2 (AA017SE->NGC009, AE192MV->ZZ001ZZ), y los 233 vehiculo_id SI resuelven. Confirma a db03 exactamente. Columna INUTILIZABLE. public.vehiculos_archive: PROD 697 filas, 0 vacias, las 697 resuelven y las 697 COINCIDEN, 0 difieren. DEV 25632 filas, 0 vacias, 25595 resuelven y las 25595 coinciden, 0 difieren. Columna CONFIABLE. O sea las dos columnas se llaman igual y una sirve y la otra no. Mi recomendacion de parkear vehiculos_archive por su propia columna se sostiene, pero AHORA ESTA MEDIDA - antes era una suposicion basada en que la columna existia, que es justo el error que db03 encontro del otro lado. DATO EXTRA de DEV que decide un caso: 37 filas de vehiculos_archive NO resuelven a ningun vehiculo vivo. Son archivos de vehiculos ya borrados, o sea para ESAS la unica llave posible es su propia columna dominio - no hay join del que sacarla. Otra razon para no reemplazarla por la derivada. ESPECIFICACION DE PARKING, una linea por tabla: rendiciones.deudas_dominios -> parkear con el dominio DERIVADO del join a public.vehiculos ANTES del delete. Su columna propia se ignora. public.vehiculos_datos_colectivos_archive -> no tiene columna dominio: parkear con el dominio DERIVADO del join ANTES del delete. En PROD las 1278 resuelven hoy; despues del truncate, ninguna. public.vehiculos_archive -> parkear con SU PROPIA columna dominio, que esta verificada. Es ademas la unica opcion para las filas cuyo vehiculo ya no existe. REGLA QUE VALE PARA CUALQUIER TABLA QUE APAREZCA DESPUES: antes de elegir una columna dominio como llave de parking, correr el test de arriba. Si no coincide con el dominio vivo en el 100 por ciento de las filas que resuelven, la llave es la DERIVADA del join y hay que capturarla antes del delete. GATE B, forma ejecutable: anti-join del parking contra los vehiculos reconstruidos POR DOMINIO, tiene que dar CERO filas sin resolver, en las tres tablas. Un conteo de filas no lo prueba: un remapeo que pierde 5 y gana 5 pasa el conteo. Control positivo: el gate falla si el parking esta vacio o si vehiculos esta vacia.
-
CUATRO HALLAZGOS DE db02-mba-l-cc que la clasificacion de FKs no alcanzaba. Dos cambian el runbook. 1. LAS TABLAS DE ARCHIVO NO ESTABAN EN NINGUNA LISTA DE DEPENDENCIAS, PORQUE NO TIENEN FK. public.vehiculos_archive: 697 filas en PROD, 25632 en DEV. public.vehiculos_datos_colectivos_archive: 1278 en PROD, 25012 en DEV. Las escribe trg_archive_*, guardan vehiculo_id SIN FK, y el rebuild las huerfana SIN NINGUN ERROR. No salen del libro. RESTRICCION DE ORDEN, OBLIGATORIA: vehiculos_archive TIENE columna dominio y se aparca como deudas_dominios. vehiculos_datos_colectivos_archive NO TIENE dominio - su unica clave es vehiculo_id. Sus 1278 filas de PROD hoy resuelven a un vehiculo vivo y despues del truncate no resuelve NINGUNA, porque el dominio no esta en la fila. Capturar el mapa vehiculo_id -> dominio ANTES del delete es obligatorio y no se puede diferir ni un paso. 2. NUNCA 'TRUNCATE ... RESTART IDENTITY'. Es la decision mas barata que hay y es una palabra. vehiculos.id es IDENTITY en los dos entornos; PROD last_value 6991 y max(id) 6991, DEV 6623/6623. Truncate SIN restart deja la secuencia quieta, los vehiculos reconstruidos arrancan en 6992 y ningun id se reusa: un vehiculo_id stale que sobreviva apunta a una fila QUE NO EXISTE y un anti-join lo encuentra. Truncate CON restart reusa los ids bajo otro vehiculo y el puntero stale queda mal, silencioso e indistinguible de uno correcto por cualquier consulta. CORRIGE UNA AFIRMACION MIA: yo escribi que el rebuild reasigna los ids y que el puntero se corrompe en silencio. Eso vale SOLO para la variante RESTART, y la variante la elegimos nosotros. La regla de aparcar por dominio sigue igual de necesaria; lo que cambia es que el modo de falla es detectable en vez de invisible, si y solo si no se usa RESTART. 3. ARGUMENTO EXTRA PARA TRUNCATE SOBRE DELETE: los triggers de archivo son BEFORE DELETE, asi que un rebuild por DELETE escribe ~4800 filas basura en esas tablas EN CADA CORRIDA. TRUNCATE no dispara nada. 4. DOS CORRECCIONES QUE NO CAMBIAN NINGUNA DECISION. rutap.m95_cambios_backfill_dev no tiene vehiculo_id: sus columnas son vehiculo_historial_id, cambios_escritos, aplicado_en, o sea sus 3787 filas cuelgan del id subrogado de vehiculo_historial, no de vehiculos. Drop-and-rebuild sigue siendo correcto. FKs entrantes a public.vehiculos en todos los schemas: 3 en PROD y 5 en DEV, no 6. Y validadoras_historial: PROD 0 filas, DEV 2 con vehiculo_id NULL - los dos teniamos razon sobre entornos distintos. GATES, con control positivo en los dos: A - triggers: baseline PROD hoy 50 triggers no-internos en public, todos tgenabled='O', cero deshabilitados. Post-rebuild tiene que leer exactamente 50 y 50 y fallar ruidosamente si no. trg_vehiculos_dominio_norm queda ENCENDIDO todo el tiempo. B - remap: anti-join de la tabla de aparcado contra los vehiculos reconstruidos POR DOMINIO, tiene que dar cero filas, para deudas_dominios y para las DOS tablas de archivo. Un conteo de filas NO prueba esto: un remap que pierde 5 y gana 5 lo pasa. Los dos gates fallan si el aparcado o vehiculos estan vacios, si no pasan por ausencia de input. NO EJERCITADO: las tablas de archivo de DEV se contaron pero no se resolvieron a dominio.
-
RUNBOOK RECOMENDADO, PROBADO EN DEV CON ROLLBACK (db02, 2026-09-03). Responde la pregunta de db03: ni (A) soltar todas las FK ni (B) DELETE con triggers apagados. Opcion C: soltar UNA sola FK. === PRIMERO, DOS DEPENDENCIAS QUE NINGUNO DE LOS TRES TENIA === El cierre RECURSIVO de referencias, derivado con una consulta y no enumerado a mano: DEV (8): vehiculos, vehiculos_datos_colectivos, vehiculo_historial, validadoras_historial, validadoras_historial_ubicaciones, rendiciones.deudas_dominios, rutap.vehiculo_historial_normalizado, rutap.m95_cambios_backfill_dev PROD (5): vehiculos, vehiculos_datos_colectivos, vehiculo_historial, validadoras_historial, validadoras_historial_ubicaciones CORRECCION A LO QUE YO ESCRIBI ANTES: dije que rutap.m95_cambios_backfill_dev 'no tiene FK y no puede tenerlo'. FALSO. Tiene FK, a vehiculo_historial por vehiculo_historial_id. Lo encontre por prueba y error, un ERROR por vez, hasta que corri el cierre recursivo y aparecieron las dos que faltaban de una. validadoras_historial_ubicaciones es de SEGUNDO nivel (referencia a validadoras_historial, no a vehiculos) y por eso no sale de ninguna lista de 'FK a vehiculos'. ENUMERAR A MANO ENCUENTRA UNA POR ERROR; LA CONSULTA RECURSIVA LAS ENCUENTRA TODAS. La consulta esta abajo. === SEGUNDO, EL TRIGGER QUE SI BLOQUEA === Corregi tambien mi propia afirmacion de que ningun trigger estorba: es cierta para las tres tablas nucleo, y FALSA una vez que el cierre arrastra validadoras. TRUNCATE sobre validadoras_historial aborta con 'validadoras_historial es append-only: inserte un nuevo evento de negocio; UPDATE, DELETE y TRUNCATE estan prohibidos' Es fn_validadoras_historial_append_only, una proteccion DELIBERADA que existe justo para impedir esto. Apagarla para pasar por arriba es exactamente lo que no hay que hacer. === EL RUNBOOK: OPCION C === validadoras_historial y su tabla hija entran al cierre por UNA sola FK. Soltando esa FK dentro de la transaccion, las dos salen del cierre, el TRUNCATE no las nombra, sus filas no se tocan y su proteccion append-only queda intacta. BEGIN; ALTER TABLE public.validadoras_historial DROP CONSTRAINT validadoras_historial_vehiculo_id_fkey; TRUNCATE public.vehiculos, public.vehiculos_datos_colectivos, public.vehiculo_historial, rendiciones.deudas_dominios, rutap.vehiculo_historial_normalizado, rutap.m95_cambios_backfill_dev; -- ... carga desde el libro, y restore del parking con vehiculo_id remapeado por dominio ... ALTER TABLE public.validadoras_historial ADD CONSTRAINT validadoras_historial_vehiculo_id_fkey FOREIGN KEY (vehiculo_id) REFERENCES public.vehiculos(id) ON DELETE RESTRICT; COMMIT; EJECUTADO EN DEV HOY DENTRO DE UNA TRANSACCION CON ROLLBACK. Resultado literal: 'TRUNCATE OK. vehiculos=0 validadoras_historial INTACTA=2', despues 'FK recreada', despues ROLLBACK. Estado posterior verificado: vehiculos=2427, vh=4341, deudas=233, valid_hist=2, FK presente=1. Nada quedo cambiado. SIN 'RESTART IDENTITY'. Deliberado: ver la nota anterior. === POR QUE NO (A) NI (B) === (A) soltar TODAS las FK entrantes: mismo resultado con superficie mucho mayor, y si algo aborta fuera de transaccion quedan constraints faltantes que nadie nota. C suelta UNA, nombrada, y la recrea en la misma transaccion. (B) DELETE con triggers apagados: dispara los triggers de archive (BEFORE DELETE) y mete ~4800 filas de basura por corrida en vehiculos_archive y vdc_archive; necesita apagar triggers, que es estado persistente si se usa ALTER TABLE; y si se usa session_replication_role='replica' se apagan TAMBIEN los triggers de FK, o sea la recarga puede insertar huerfanos sin que nadie se entere. Ademas mi rol no puede setear ese parametro en DEV ('permission denied to set parameter session_replication_role'), asi que requiere superusuario. === vh_normalizado y m95 === Los dos son scratch derivado del import, en el esquema rutap, reconstruibles desde el snapshot elegido, ninguno es fuente de verdad. Entran en la lista del TRUNCATE y NO se parkean. m95 ademas queda invalidada igual porque su llave es vehiculo_historial_id y ese id se reasigna. === LA CONSULTA DEL CIERRE, para re-derivarla en cada entorno y en cada corrida === WITH RECURSIVE core(t) AS ( SELECT 'public.vehiculos'::regclass UNION SELECT 'public.vehiculos_datos_colectivos'::regclass UNION SELECT 'public.vehiculo_historial'::regclass UNION SELECT c.conrelid FROM pg_constraint c JOIN core ON c.confrelid=core.t WHERE c.contype='f' AND c.conrelid<>c.confrelid) SELECT t::text FROM core ORDER BY 1; GATE C, nuevo: el cierre re-derivado el dia de la corrida tiene que ser IGUAL a la lista del runbook. Si aparece una tabla nueva, el runbook para hasta clasificarla.
-
GATE AUTO-EJECUTABLE, VERIFICADO (db02, 2026-09-03, DEV con ROLLBACK). coder02 senialo que la FK recreada YA ES el gate que yo pensaba escribir a mano para validadoras_historial. Lo probe en vez de aceptarlo, y es cierto. PRUEBA (tablas temporales descartables, para no depender de los triggers de validadoras): padre con 1 fila, hijo con 1 fila que la referencia, FK creada OK DROP CONSTRAINT, TRUNCATE del padre -> el hijo queda con 1 fila huerfana ADD CONSTRAINT -> 'ERROR: insert or update on table t_hijo violates foreign key constraint fk1. DETAIL: Key (pid)=(1) is not present in table t_padre' transaccion abortada, ROLLBACK O sea ADD CONSTRAINT ... FOREIGN KEY VALIDA las filas existentes (no es NOT VALID por defecto). Aplicado al runbook: si al momento de la corrida validadoras_historial tiene una fila con un vehiculo_id real y el rebuild no recrea ese vehiculo, la ALTER final FALLA y TODA la transaccion se revierte - truncate incluido. PROMOVIDO DE CHEQUEO MANUAL A PROPIEDAD DEL RUNBOOK: retiro mi 'RE-MEDIR el conteo de validadoras_historial el dia de la corrida'. No hace falta acordarse. La transaccion NO PUEDE COMMITEAR dejando una validadora huerfana. Es la diferencia entre un gate que hay que recordar y uno que no se puede saltear, y es la razon por la que soltar-y-recrear-UNA-FK es mejor que soltar todas: cada FK recreada se auto-verifica. Hoy pasaria igual: PROD tiene 0 filas y las 2 de DEV tienen vehiculo_id NULL, y NULL no viola una FK. NOTA DE METODO, del intento fallido: mi primera prueba quiso sembrar una fila en validadoras_historial y la rechazo fn_validadoras_historial_validar ('No se pudo resolver el tipo/modalidad activos de la validadora'). Esa tabla tiene TRES triggers (append_only, no_truncate, validar) y no se deja usar como banco de pruebas. Por eso la prueba final va sobre tablas temporales: lo que se estaba probando es una propiedad de Postgres, no de esa tabla, y elegir el sujeto mas simple la prueba mejor. Estado DEV verificado despues de las dos pruebas: vehiculos=2427, valid_hist=2, con vehiculo_id no nulo=0, FK presente=1. Nada quedo modificado.
-
EL BLOQUEO DE db03 ESTA BIEN Y ADEMAS ES MAS CHICO DE LO QUE PARECE (db02, 2026-09-03, SELECT + inventario de dev/dev). CONFIRMO LA AUSENCIA, enumerada y con control positivo. Todo archivo de dev/dev que hace INSERT en public.vehiculos / vehiculos_datos_colectivos / vehiculo_historial vive en docs/migrations/ y son 13 one-offs fechados entre el 08-13 y el 08-28. En tools/ no hay ninguno. Control positivo sobre el mismo arbol y el mismo comando: el patron de UPDATE devuelve 12+ archivos, asi que el cero de INSERT es del mundo, no del patron. tools/rutap-sync-prod.sql: CERO 'insert into'. Solo 3 UPDATE sobre tablas public (vdc x2, vehiculo_historial x1). Confirma lo que reporto db03. PERO EL GREP DE SQL NO ALCANZA AL IMPORTER DE PYTHON, y ahi esta la mitad buena. tools/rutap-snapshot-import.py (1018 lineas) escribe SOLO en el esquema rutap: rutap.snapshots, rutap.rutap_rows, rutap.rutap_expedientes. Nunca toca public. O sea es un cargador de STAGING, no de nucleo, y esta VIVO Y AL DIA. ESTADO MEDIDO DEL STAGING EN DEV: rutap.snapshots: 1 fila, id=9, archivo 2026-09-03-1049-parque-rutap.xlsx, filas=5368 rutap.rutap_rows: 5368 filas rutap.vehiculo_historial_normalizado: 5368 filas (mas chasis_resolver 9, unidad_propulsora_resolver 4, m95 3787) Las 5368 coinciden EXACTO con mi propio conteo de filas del libro con Nro_dominio no vacio. El libro vigente ya esta cargado y normalizado. ENTONCES EL HUECO NO ES 'FALTA UN IMPORTADOR'. Es UN PASO: la proyeccion rutap.vehiculo_historial_normalizado -> public.vehiculos / vdc / vehiculo_historial, insert-only. La mitad libro->staging existe, esta auditada y trae el libro del 09-03. RESTRICCION DE SECUENCIA QUE ESTO CREA, y es nueva: rutap.vehiculo_historial_normalizado esta DENTRO del cierre del TRUNCATE (tiene FK a vehiculos) Y es la FUENTE de la recarga. Truncarla en la misma transaccion que el nucleo borra el insumo del load. Dos salidas: (a) excluirla del TRUNCATE - se puede, soltando su FK igual que la de validadoras; o (b) truncarla y reconstruirla desde rutap.rutap_rows antes del paso de carga. rutap.rutap_rows NO esta en el cierre (no tiene FK), asi que sobrevive al truncate y es el staging durable del que se puede regenerar todo. CONVIERTO G5 EN PRE-CONDICION, no en chequeo posterior: el runbook NO trunca hasta que exista un loader que demuestre recargar. Un gate que corre despues no sirve de nada si el nucleo quedo vacio - es exactamente el caso 'el gate pasa por falta de entradas' que este item ya nombra. La forma correcta es: cargar a una tabla de prueba desde el staging, contar, y recien entonces habilitar el truncate. NO EJERCITADO: no lei el cuerpo de rutap.vehiculo_historial_normalizado columna por columna para decir si alcanza para poblar las tres tablas nucleo; no mire el esquema rutap en PROD (que puede no existir, como no existe rendiciones).
-
CORRECCION AL RUNBOOK (db02, 2026-09-03): rutap.vehiculo_historial_normalizado esta en la clausura del truncate Y es la fuente de recarga obvia. Truncarlo en la misma transaccion destruye el insumo de la carga. El loader arranca de rutap.rutap_rows, que no tiene FK, no esta en la clausura y sobrevive. Y el gate G5 se mueve de post-check a PRE-condicion: no se trunca hasta que un loader demuestre recarga contra una tabla scratch, contada. Un gate posterior no sirve si el nucleo quedo vacio. Ver MOVILBA-227.
-
db02, gates del rebuild DEV, 2026-09-03. Tres numeros del GO son de PROD y no valen en DEV (verificado 10.9.10.54 movilba_dev PG15.17). Solo lecturas. 1. 'triggers 50/50' no es de DEV: DEV tiene 7 triggers no internos en las 3 tablas nucleo (7 habilitados) y 13 en el cierre entero (13 habilitados). El gate se redacta 'habilitados = total, re-derivado en ejecucion, Y FALLA SI EL TOTAL ES CERO' - un 0/0 pasa sin probar nada. 2. MOVILBA-200: 574 es PROD, DEV tiene 584 vehiculos apuntando a un chasis borrado. Denominador por entorno. 3. HUECO EN LA PRE-CONDICION 4, y es mio. La regla 'tomar el mapa vehiculo_id -> dominio de la FICHA VIVA' la derive en PROD, donde es total (9 dominios duplicados, los 9 con ficha viva). En DEV NO: 11 duplicados y 2 SIN ficha viva en ninguna fila, o sea el desempate no tiene de donde elegir. DEV tiene 32 vehiculos sin ficha viva contra 10 de PROD. Es mi propia trampa al reves: medi en PROD y entregue la propiedad como si fuera de la operacion, y la operacion corre en DEV. Los dos casos: AI088HH (v.id 5230/5245, sin ficha, AMBOS subtipo 18 - el valor parkeado no es ambiguo) y NEA182 (8 filas, 8 subtipos distintos 17/18/5/9/2/1/4/18, 5 estados, todas las fichas borradas - los valores se contradicen y nada desempata). NINGUNO DE LOS DOS ESTA EN EL LIBRO: 0 filas en rutap.rutap_rows en DEV y en PROD, denominador 5368 en ambos. No sobreviven al truncate-and-reload, asi que el radio de esta corrida es cero. Pero la REGLA queda con un caso indefinido y hay que escribirla asi: parkear desde vehiculos, usando la ficha viva para elegir ganador cuando el dominio tiene varias filas; SI NINGUNA FILA TIENE FICHA VIVA Y LOS SUBTIPOS SE CONTRADICEN, PARAR Y CLASIFICAR. Un subtipo equivocado satisface NOT NULL y la FK y no levanta nada. LO QUE SI VALE EN LOS DOS ENTORNOS: cero dominios con mas de una ficha VIVA y cero vehiculos con mas de una ficha viva. Es la condicion que hace valido el parking - se ASERTA en el gate, no se asume. NO EJERCITADO: el cierre recursivo re-derivado (pre-condicion 3, de db03 en ejecucion); los gates de 213 y 216 contra DEV; si los 32 vehiculos sin ficha viva de DEV estan respaldados por el libro. Cero escrituras.
-
db02, gate E (post-rebuild: cero vehiculos con dominio ausente del libro), denominador derivado ANTES de escribir el gate. Solo lecturas. DENOMINADOR: DEV 28 vehiculos en 19 dominios ausentes del libro. PROD 0. 1. GATE E ES VACUO EN PROD: el pre-estado ya es cero, asi que en PROD pasa sin probar nada - la misma forma 0/0 contra la que se escribio la regla. Necesita la segunda clausula del gate A: FALLAR SI EL DENOMINADOR DEL PRE-ESTADO ES CERO, capturado ANTES del truncate. En DEV el control es real (28). 2. LOS 19 DOMINIOS SON TRES POBLACIONES, no una, y solo la primera es MOVILBA-152: - ARTEFACTOS DE IMPORT, 11 dominios, creado_por=system_import, NINGUNO con ficha viva: AE111LL/AE222LL/AE333LL (secuenciales), AG358KK, AH105RH, AI088HB, AI088HH, AZE323, KRL571, NEA182, RPY719. - HUMANOS, 6 dominios, creado_por es un CUIT (27302187695, 20421184020, 20394678369, 20247094211): AA111EE, ABC1234, DHU474, PAN219, TT628PA, YAM567. - DEMO, 2 dominios, creado_por=demo-movilba-88: ZZ001ZZ, ZZ002ZZ. CINCO TIENEN FICHA VIVA Y EL REBUILD LOS BORRA: AA111EE, DHU474, PAN219, ZZ001ZZ, ZZ002ZZ. Todos los artefactos de import ya estan soft-deleted; los vivos son humanos o demo. 'las filas fuera del libro no vuelven' es cierto y ademas se lleva 5 filas vivas creadas por personas que nadie clasifico. Es una consecuencia a declarar antes de la transaccion, no a descubrir en el gate. 3. CAVEAT SOBRE MI PROPIA EVIDENCIA: creado_por lo elige quien escribe y no prueba origen (trampa estandar del proyecto). El corte de tres poblaciones correlaciona limpio con tener ficha viva, y por eso vale reportarlo, pero es una PISTA para clasificar, no la clasificacion. 4. DOS CORRECCIONES AL RATIONALE DE 152, ninguna cambia la conclusion: NEA182 tiene CINCO fechas de creado_en distintas (2025-12-11, 12-15, 12-16, 2026-01-22, 02-25), no cuatro. Y TRES de las ocho filas se crearon el MISMO dia (2025-12-11) con tres subtipos distintos (17, 18, 5): el defecto es DENTRO de una corrida ademas de entre corridas, lo que refuerza 152. GATE E COMO QUEDA: capturar el conteo del pre-estado ANTES del truncate, fallar si es cero, asertar cero despues, y ENUMERAR en el reporte las filas con ficha viva para que el borrado quede declarado y no implicito. NO EJERCITADO: no clasifique ninguno de los 19; no chequee si los 6 dominios humanos estan en un libro ARCHIVADO (solo contra el snapshot vigente de 5368); no mire los otros 23 vehiculos sin ficha viva de DEV. Cero escrituras.
-
db02: gates escritos y PROBADOS. Dos artefactos en docs/migrations/, ninguno commiteado (coder02 los sube por path explicito). Cero escrituras a base. 1. VOLCADO OBLIGATORIO tomado: docs/migrations/2026-09-03-2328-dev-dominios-fuera-del-libro-DUMP.json, md5 9781bc397d8248ce2c2c5efe3b81ed98, 70049 bytes. 28 vehiculos / 25 vdc / 18 historial / 19 dominios, todas las columnas. Entorno y denominador del libro (5368) estampados adentro; los 5 dominios con ficha viva en un campo propio. Armado con json_agg del lado servidor para que los saltos de linea de observaciones no partan filas. Verificado releyendolo. 2. GATES: docs/migrations/2026-09-03-movilba-226-gates-rebuild-DEV.sql. Dos secciones (-v seccion=pre antes del truncate, -v seccion=post despues). NINGUN GATE LLEVA UN LITERAL: el cierre FK se re-deriva por consulta recursiva en ejecucion y todo denominador del POST viene de la corrida PRE. Gates A triggers (habilitados=total, falla si total=0 y si el conteo cambio), B MOVILBA-200, D dominio-es-llave-de-la-ficha-viva, E dominios fuera del libro, F parking restituido + cero huerfanos en deudas_dominios, G la FK soltada recreada. PRE corrio limpio en DEV: cierre re-derivado a las mismas 8 tablas, triggers 13, chasis_borrado 584, no_libro 28, y las 5 bajas enumeradas por nombre. 3. EL CONTROL POSITIVO ENCONTRO UN DEFECTO EN MI PROPIO ARCHIVO DE GATES. Corri el POST contra DEV ANTES de cualquier rebuild, donde B y E TENIAN que fallar. No fallaron: no se ejecutaron. Una variable :var de psql NO se interpola dentro de un cuerpo dollar-quoted, asi que A, B y E murieron con 'syntax error at or near :' - y psql, sin ON_ERROR_STOP, paso por encima de los tres e imprimio '=== TODOS LOS GATES PASARON ==='. Un script de gates que anuncia exito con gates sin ejecutar es peor que no tener gates: es la misma forma que todo lo de hoy (el chequeo da limpio porque nunca tuvo entrada), salvo que aca el instrumento era justo lo construido para atajar eso. Arreglado con \set ON_ERROR_STOP on + variables materializadas en temp table fuera del dollar-quote. Re-corrido: A pasa (correcto, los triggers no cambiaron), B ABORTA con '584 vehiculos siguen sobre un chasis borrado (antes 584)', exit 3, se detiene. E probado aislado (esta aguas abajo de B): aborta con '28 vehiculos sobre un dominio que no esta en el libro' y su rama de vacuidad aborta con 'pre-estado 0 - es el caso de PROD'. Las dos ramas disparan. Los gates estan demostrados como DETECTORES, no solo como ejecutables. Es la unica afirmacion que hago sobre ellos. NO CUBIERTO (en el footer del propio archivo): los 32 casos de MOVILBA-213 y las 170 filas de MOVILBA-216, hasta que cada uno tenga consulta acordada. NO EJERCITADO: ningun gate corrio contra un estado POST-rebuild porque no existe todavia; el chequeo de huerfanos del gate F cubre solo deudas_dominios, no las otras tres tablas parkeadas, que necesitan los nombres que cree el loader de db03.
-
db04-mba-l-ca
-
db04-mba-l-ca 2026-09-24 04:05 -03 — Status sweep limitation: The rebuild gate for trigger behavior, validator history and re-enable/fidelity was not exercised; this SELECT-only sweep cannot rehearse TRUNCATE or trigger behavior. BWI show and recent events were read; status unchanged and no DB write.
-
db03-mba-l-cc / db
-
db03 2026-10-08: premise superseded, closing. Re-derived against the CURRENTLY SHIPPED pipeline, not the 2026-09-03 design this ticket's gates were written for. 226's gates (trigger-disable non-issue, validadoras_historial FK-drop/recreate as re-enable gate, parking/remap of orphaned archive rows) were all written for a TRUNCATE-and-reload rebuild of public.vehiculos/vehiculos_datos_colectivos/vehiculo_historial, designed 2026-09-03. That design was abandoned the NEXT day. MOVILBA-227 (status=done, same parent MOVILBA-155), dated 2026-09-04, states explicitly: "Model: id-preserving UPSERT/reconcile. It deliberately does NOT TRUNCATE, DELETE, CASCADE, disable triggers, or disable constraints." 227 is the loader actually shipped: tools/rutap-snapshot-import.py:1253 calls `rutap.proyectar_snapshot(snap)` on every import run. Verified today: `pg_get_functiondef('rutap.proyectar_snapshot')` contains zero occurrences of TRUNCATE. The function does equality-join LEFT JOINs and INSERTs into rutap.vehiculo_historial_normalizado (see MOVILBA-290 closure, same session), never touches public.vehiculos/vdc core tables by truncation, never drops/recreates the validadoras_historial FK, never disables a trigger. Every premise this ticket's gates exist to protect against (trigger auto-disable by TRUNCATE, validadoras_historial orphaning, archive-table orphaning from a rebuild, re-enable-after-rebuild fidelity) cannot occur under the installed architecture, because there is no rebuild/truncate step in it. The gates themselves (docs/migrations/2026-09-03-movilba-226-gates-rebuild-DEV.sql) remain on disk as a correct, tested artifact for a design that was not adopted - historical record, not a live gap. NOT re-measured: whether any FUTURE pipeline change reintroduces a truncate/rebuild step (none exists today, no need to gate against a hypothetical). No DB writes this ticket.
-
Premise superseded: shipped loader (MOVILBA-227, rutap.proyectar_snapshot) is id-preserving UPSERT/reconcile, no TRUNCATE - this ticket's rebuild-gates design was abandoned the day after it was written. Verified function body has zero TRUNCATE occurrences.
-
audit pass: re-ran pg_get_functiondef TRUNCATE check on PROD
payload
{"scope":"re-ran pg_get_functiondef TRUNCATE check on PROD","outcome":"pass","evidence":"rutap.proyectar_snapshot(bigint) body contains zero TRUNCATE occurrences, confirmed live","reportRef":null}