Este ensayo se publico primero en brianmenagomez.com/blog/por-que-cod-iae-normalized-no-es-unico.
La suposición que no verifiqué
Hay un campo en la base de Conversor que se llama `cod_iae_normalized`. Lo creé para tener el código del IAE en un formato limpio: sin ceros a la izquierda, sin guiones, sin espacios. Y, como el propio nombre lo prometía, lo traté como si fuera único. Un código, una fila. Sobre esa premisa monté el endpoint que devuelve una actividad económica por su código.
La premisa era falsa.
El IAE no es una lista plana. Está organizado en secciones —la A, la B y la C— y un mismo número puede aparecer en más de una. Cuando por fin miré la base con calma, encontré unos 110 códigos que colisionan. Ciento diez números que no identifican una sola actividad, sino dos o tres según la sección en la que caigan.
Me costó verlo porque no lo busqué antes. Di por bueno el nombre de una columna en lugar de comprobarlo contra los datos. Es el tipo de error que no falla en el primer momento: falla lo que creías saber de tu propia base. Cuando algo se llama "normalized", tiendes a leer "único", y esa lectura era exactamente lo que no era.
Dos filas para el mismo 831
El ejemplo que me lo dejó claro no lo voy a olvidar. El número 831 existe dos veces, en dos secciones distintas. En la sección A, el A831 son «Auxiliares financieros». En la sección B, el B831 son «Médicos».
Mismo número, dos actividades que no tienen nada que ver entre sí. Si un banco regional español te pregunta por el 831 y tú respondes con una sola fila, la mitad de las veces vas a contestar mal. No porque el dato esté mal, sino porque la pregunta estaba incompleta: le faltaba la sección.
Ahí entendí de una vez que normalizar no es lo mismo que hacer único. Normalizar significa limpiar el formato y dejarlo consistente. La unicidad es otra cosa: es una propiedad del modelo de datos, una decisión sobre qué identifica de verdad a cada fila. Yo había confundido las dos. Al construir el campo normalizado y sacarlo del contexto de su sección, dejé el 831 desnudo, listo para colisionar con su gemelo.
`.maybeSingle()` y el 500 que no avisaba
El helper `.maybeSingle()` parecía hecho para mi caso. Le pides una consulta y te devuelve la fila si hay exactamente una, o nada si no la hay. Perfecto para una columna que crees única.
Lo usé sin pensarlo dos veces. La consulta filtraba por `cod_iae_normalized` y `.maybeSingle()` cerraba el asunto. Fin de la historia, creía yo.
El problema es lo que `.maybeSingle()` hace cuando hay dos o más. No devuelve la primera. No devuelve un error amable con un mensaje explicativo. Devuelve `PGRST116`, «multiple rows», y eso se convierte en un 500 en el endpoint.
La primera vez que un código colisionante llegó hasta ahí no hubo aviso. Hubo un 500 seco, de esos que te obligan a abrir los logs para entender qué ha pasado. Y cuando los abrí, el error me estaba contando algo más grande que un fallo técnico: me estaba contando que mi modelo mental de la base estaba mal. Yo había construido el endpoint sobre una columna que no era lo que yo creía. El 500 no era el bug. El bug era mi suposición, y el 500 solo la estaba señalando.
Leer todas las filas y elegir por `id_iae`
Arreglarlo no fue cambiar `.maybeSingle()` por `.single()` ni meter un `LIMIT 1` a la desesperada. Fue cambiar la pregunta que le hago a la base.
Antes preguntaba "dame la fila de este número". Ahora el núcleo lee todas las filas de ese código en todas las secciones. Todas, sin excepción. Y la ambigüedad se resuelve de forma explícita, no por accidente: cuando la petición llega por un path que identifica la sección exacta, elige la fila de esa sección —la A, por ejemplo— y deja las hermanas de las otras secciones a la vista, en un campo `alternatives`.
Leer todas las filas no basta para devolver la correcta de forma fiable. El cierre viene después: re-selecciono por `id_iae`, que sí es único, y que me lo devuelve la RPC. El código normalizado se queda en lo que siempre fue —un índice de búsqueda cómodo— y la unicidad se apoya donde de verdad existe, en `id_iae`.
Dicho así suena a cambio pequeño. No lo fue. Significó aceptar que la columna con el nombre bonito no era una clave, y reordenar la lógica del núcleo para que la sección y el `id_iae` cargaran con la responsabilidad de identificar. La colisión no desaparece: el 831 sigue existiendo dos veces, y está bien que sea así. Lo que cambié es que el sistema ya no finge que no pasa. Antes mentía por omisión y devolvía un 500; ahora lee el doble, lo muestra y devuelve la fila que de verdad te importa.
Lo que me llevo
Normalizado no es único. Un campo puede tener el formato impecable y seguir sin servir como clave, porque la unicidad no la decide el nombre de la columna: la decide el modelo de datos. Y la única forma de saberlo es mirar los datos, no el nombre.
De ahí la regla de oficio que ahora aplico siempre: antes de montar una consulta sobre una columna, compruebo contra la base real si esa columna es única de verdad. No le pregunto al nombre, le pregunto a los datos. Yo no lo hice y me costó un 500 y una tarde de logs. No volverá a pasar.
Si quieres ver cómo quedó el endpoint y cómo devuelve las `alternatives`, está en los docs de la API: https://www.conversoriaecnae.es/api/v1/docs

