Estado: ✅ Aceptada Fecha: 2026-05-28 Bloque: X2 (segundo sub-bloque del bloque X del roadmap) Bump on-disk: ninguno (X1 ya tenía el slot de timing en TriggerMeta)

🧭 Contexto

X1 (ADR-0029) entregó triggers AFTER con body de una sola sentencia. X2 cierra los dos huecos más visibles:

  1. BEFORE triggers: para validaciones que abortan la operación (vía error propagado) y para logs que precedan al write.
  2. Body multi-statement BEGIN ... END: para triggers que necesitan hacer más de una cosa (típico: log + denormalización + counter increment).

Lenguaje procedural completo (variables, IF/THEN, LOOP) y CREATE FUNCTION quedan para X3+.

💡 Decisión

1. BEFORE triggers habilitados

CREATE TRIGGER ... BEFORE ... ya no rebota — X1 lo rechazaba al CREATE-time con [GBY-4093]. Ahora se persiste y se dispara en los hooks BEFORE de exec_insert/exec_update/exec_delete.

Cómo se construyen NEW/OLD para BEFORE:

Evento NEW OLD
BEFORE INSERT User-stated cols + NULL para columnas no especificadas —
BEFORE UPDATE OLD con las assignments evaluadas contra OLD (sin tocar disco) Snapshot del row antes del update
BEFORE DELETE — Snapshot del row antes del delete

Limitación X2 importante: NEW es read-only desde el trigger BEFORE — no se puede mutar la fila desde el body. Esto significa que BEFORE en X2 NO sirve para “rellenar defaults” o “modificar la fila antes de escribir”; sirve para auditar pre-write o abortar (vía error propagado).

Aborto vía error: si el body de un BEFORE trigger falla (e.g. INSERT con PK duplicada en otra tabla), el error se propaga y el INSERT/UPDATE/DELETE original aborta — el wrap de transacción del caller hace rollback. Es la mecánica de “validación con BEFORE” hecha por composición.

2. Body multi-statement BEGIN ... END

Nueva sintaxis:

CREATE TRIGGER multi AFTER INSERT ON t FOR EACH ROW BEGIN
    INSERT INTO log_a (id) VALUES (NEW.id);
    INSERT INTO log_b (id) VALUES (NEW.id);
END;

Cada substatement se separa por ;. Pueden anidarse BEGIN ... END (depth tracking en parser), aunque en X2 no agrega expresividad (no hay scope, no hay control de flujo).

Cambios estructurales:

3. Reglas del body single-statement preservadas (back-compat con X1)

Triggers creados con X1 (body sin BEGIN/END) siguen funcionando idénticamente. El parser detecta:

📐 Códigos de error

Mismos que X1 — no se agregaron códigos nuevos. [GBY-4093] ahora cubre también:

🧪 Validación

Suite x2_* en tests/integration_test.rs (7 tests):

Plus se removió x1_before_rejected_in_release (la restricción que el test verificaba fue lifteada).

Suite total: 423/423 pass (cargo test --lib --tests).

🔭 Futuro (X3+)