Convención de nombres para triggers en PostgreSQL (Supabase)
Por qué PostgreSQL obliga a construir lo que otras bases dan por defecto — y una convención de nombres para funciones trigger y triggers que escala para ingenieros y agentes de IA en Supabase.
Ender Puentes
Contexto: por qué terminé pensando en esto
Durante los últimos meses trabajé en un proyecto que me desafió en la capa de datos. La base de datos era PostgreSQL ejecutándose en Supabase — un conjunto tecnológico sólido, pero con una fricción que no anticipé al provenir de ORMs y bases de datos más opinionadas.
Cosas que en otros entornos son triviales — mantener updated_at al día, inicializar filas relacionadas al insertar, propagar cambios de estado, limpiar recursos al borrar — en PostgreSQL no existen por definición. No hay ON UPDATE CURRENT_TIMESTAMP universal ni mecanismos declarativos de alto nivel. Si se requiere ese comportamiento, hay que construirlo: una función PL/pgSQL y un trigger que la ejecute en el momento correcto del ciclo de vida de la fila.
Eso escala con rapidez. Una tabla con auditoría básica ya tiene un BEFORE UPDATE. Si además se sincronizan entidades relacionadas, se encolan efectos secundarios o se validan invariantes, se pasa de uno a tres o cuatro triggers por tabla. Sin convención, el catálogo se llena de trg1, handle_user, set_timestamp y funciones cuyo nombre no indica en qué tabla operan ni cuándo se ejecutan.
Fue entonces cuando empecé a pensar en cómo nombrar tanto los triggers como sus funciones: no como capricho estético, sino como herramienta para que quien lea el esquema después — ya sea una persona o un agente — pueda orientarse sin abrir cada cuerpo PL/pgSQL.
La convención que adopté
Separo dos artefactos que PostgreSQL permite mezclar en un solo bloque, pero que en la práctica conviene tratar distinto:
- Trigger (objeto DDL) · Enlace: tabla, evento, timing, condición WHEN — {evento}_{timing}_{tabla}
- Función trigger (cuerpo PL/pgSQL) · Lógica ejecutada por el motor — trigger_{evento}_{timing}_{tabla}
Donde:
- evento: insert | update | delete
- timing: before | after
- tabla: nombre exacto de la tabla en snake_case (users, order_items, …)
Ejemplo — el caso que me empujó a estandarizar: auto-actualizar updated_at:
-- Enlace: cuándo y dónde se asocia
CREATE OR REPLACE TRIGGER "update_before_users"
BEFORE UPDATE ON "public"."users"
FOR EACH ROW
EXECUTE FUNCTION "public"."trigger_update_before_users"();
-- Cuerpo: qué hace
CREATE OR REPLACE FUNCTION "public"."trigger_update_before_users"()
RETURNS "trigger"
LANGUAGE "plpgsql"
SET "search_path" TO 'public'
AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$;La función lleva el prefijo trigger_; el objeto trigger no. Esa diferencia de un token es intencional.
Organización sugerida en el repositorio (adaptable):
schemas/
triggers/{tabla}.sql ← solo DDL de enlace
functions/triggers/{tabla}/ ← un archivo por función
trigger_update_before_{tabla}.sqlPor qué uso esta sintaxis (opinión)
1. PostgreSQL exige triggers; los nombres deben compensar esa fricción
Cuando updated_at no se actualiza por sí solo, cada tabla repite el mismo patrón con variaciones. Sin convención, se termina copiando fragmentos de Stack Overflow con nombres distintos en cada migración. Con {evento}_{timing}_{tabla}, el patrón resulta reconocible de inmediato: «este es el disparador de UPDATE BEFORE sobre users», sin necesidad de abrir el archivo.
2. El nombre sigue el orden en que pienso el problema
Leo insert_after_orders como: «cuando ocurre un INSERT, después de persistir la fila, sobre `orders`».
No imito el orden literal de CREATE TRIGGER … AFTER INSERT ON … (timing → evento → tabla), pero sí el flujo mental del dominio: qué ocurrió → cuándo en el ciclo de vida de la fila → dónde. Eso reduce la fricción al depurar: el stack trace, \dy en psql y el árbol de archivos hablan el mismo idioma.
3. Prefijo trigger_ en funciones para no competir con RPC expuestas
En Supabase, las funciones del esquema public pueden exponerse vía PostgREST como RPC. Las funciones de negocio siguen verbos explícitos: create_order, get_user_by_id. Si la función del trigger se llamara igual que el trigger (update_before_users), en pg_proc convivirían dos familias de objetos con nombres casi idénticos pero roles opuestos: invocables desde el cliente frente a ejecutables únicamente por el motor.
El prefijo trigger_ indica de forma inequívoca: esto no es una RPC; no debe exponerse; no debe invocarse desde la capa de aplicación. Es un espacio de nombres de bajo costo dentro del mismo esquema.
4. Separación física entre enlace y cuerpo
- Triggers → solo el cableado (tabla, timing, WHEN).
- Funciones → lógica, efectos secundarios, llamadas a otras funciones, validaciones.
Ventajas concretas:
- Un cambio de lógica no necesariamente modifica el contrato del trigger.
- Es posible listar todos los disparadores de una tabla en un archivo pequeño.
- Escala cuando una tabla acumula varios triggers — algo habitual cuando PostgreSQL sustituye funcionalidades que otros motores incluyen de forma nativa.
5. Correspondencia 1:1 predecible
Regla simple: nombre del trigger + prefijo `trigger_` = nombre de la función.
update_before_users → trigger_update_before_users
insert_after_orders → trigger_insert_after_orders
delete_before_projects → trigger_delete_before_projectsEso elimina la ambigüedad clásica en la que el trigger se llama trg_users_upd y la función fn_set_updated_at. No existe una capa intermedia de traducción mental.
6. Nombres explícitos, sin abreviaturas crípticas
Un trigger no es el lugar adecuado para creatividad onomástica. Si en el futuro un agente de IA o un desarrollador nuevo busca update_before_, debe encontrar exactamente lo esperado — no ub_u_ts ni "Set Updated At".
Ventajas con criterio
Para agentes de IA
- Regularidad sintáctica — El patrón {evento}_{timing}_{tabla} es analizable con expresiones regulares; un agente puede inferir nombres sin leer todo el esquema.
- Inferencia de rutas — Dado users + update + before, la ruta del archivo es determinista si el repositorio sigue la estructura por entidad.
- Disambiguación RPC vs trigger — El prefijo trigger_ evita que un LLM genere una RPC update_before_users o confunda una función trigger con un endpoint PostgREST.
- Ediciones acotadas — Una solicitud del tipo «cambiar la lógica del update before de users» acota el alcance a un archivo y a un identificador conocidos.
- Diferencias legibles — Los cambios de lógica y los cambios de cableado del trigger producen diferencias en archivos distintos; el impacto se evalúa con mayor rapidez.
- Patrones repetibles — El caso updated_at se replica en decenas de tablas con el mismo nombre; un agente aprende el patrón una vez y lo aplica en todas.
Limitaciones que conviene documentar en el proyecto:
- PostgreSQL trunca identificadores a 63 bytes. Las tablas con nombres extensos producen nombres recortados en el catálogo. Conviene verificar \df / \dy ante cualquier duda.
- Las condiciones WHEN no forman parte del nombre; residen en el DDL del trigger.
- Los esquemas especiales (p. ej., auth en Supabase) pueden requerir variantes; conviene documentarlas como excepción, no como patrón general.
Para el ingeniero de software
- Lectura en catálogo — \dy+ users y \df trigger_*users* muestran un vocabulario coherente.
- Orden alfabético útil — Agrupa por evento/timing: todos los update_before_* quedan juntos.
- Incorporación al equipo — La regla cabe en una sola frase; no hace falta un wiki aparte.
- Depuración — Un error del tipo function trigger_update_before_users() does not exist apunta directamente al archivo esperado.
- Revisión de código — Si la solicitud de integración renombra la función pero no el trigger (o viceversa), la inconsistencia resulta evidente.
- Migraciones — DROP TRIGGER IF EXISTS update_before_users ON public.users resulta autodescriptivo en el historial de git.
- Supabase / esquema declarativo — Separar enlace y cuerpo encaja con flujos de esquema como código y con diferencias revisables en solicitudes de integración.
Casos de uso típicos (donde la convención aporta valor)
- Mantener updated_at · update_before_{tabla} — No existe actualización automática universal en columnas timestamp
- Inicializar filas relacionadas al insertar · insert_after_{tabla} — No hay cascada declarativa para lógica de negocio
- Validar invariantes antes de persistir · update_before_{tabla} / insert_before_{tabla} — Las restricciones CHECK cubren menos que la lógica entre tablas
- Efectos secundarios post-commit (colas, notificaciones) · insert_after_{tabla} / update_after_{tabla} — No existe un bus de eventos nativo
- Limpieza al borrar · delete_before_{tabla} / delete_after_{tabla} — FK CASCADE no basta para recursos externos (almacenamiento, colas, secretos)
Anti-patrones que evito
- Nombres en prosa ("Sync Audience", "Set Updated At") — No escalan, dificultan la búsqueda, generan problemas en migraciones
- Abreviaturas opacas (trg_u_ins, fn1) — Requieren documentación paralela
- Mismo nombre para trigger y función sin prefijo — Colisión semántica con RPC; confusión en PostgREST
- Un archivo monolítico con todos los triggers — Diferencias ruidosas, difíciles de revisar
- Lógica de negocio pesada dentro del trigger — El nombre identifica el disparador; la complejidad debe ir a funciones auxiliares
Referencia rápida
Trigger: {insert|update|delete}_{before|after}_{tabla}
Función: trigger_{insert|update|delete}_{before|after}_{tabla}
Regla: nombre_función = "trigger_" + nombre_trigger