# Convención de nombres para triggers en PostgreSQL (Supabase)

PostgreSQL no actualiza timestamps ni limpia filas relacionadas solo — hay que cablearlo con triggers. Una convención de nombres para que el catálogo se entienda rápido, sea una persona o un agente de IA en Supabase.

## 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 en Supabase — un stack sólido, pero con una fricción que no anticipé viniendo de ORMs y bases 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:

```sql
-- 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):

```text
schemas/
  triggers/{tabla}.sql              ← solo DDL de enlace
  functions/triggers/{tabla}/       ← un archivo por función
    trigger_update_before_{tabla}.sql
```

## Por 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**.

```text
update_before_users     →  trigger_update_before_users
insert_after_orders     →  trigger_insert_after_orders
delete_before_projects  →  trigger_delete_before_projects
```

Eso 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

```text
Trigger:   {insert|update|delete}_{before|after}_{tabla}
Función:   trigger_{insert|update|delete}_{before|after}_{tabla}
Regla:     nombre_función = "trigger_" + nombre_trigger
```
