Estado: ✅ Aceptada Fecha: 2026-05-28 Bloque: X3 (tercer sub-bloque del bloque X del roadmap) Bump on-disk: VERSION 14 → 15 (nuevo ObjectKind::Procedure en el catálogo)

🧭 Contexto

X1 (ADR-0029) y X2 (ADR-0030) entregaron triggers AFTER/BEFORE con body single-stmt y multi-stmt. X3 entrega la pieza siguiente del bloque X: stored procedures con CALL.

Funciones escalares invocables desde SELECT (CREATE FUNCTION RETURNS scalar) quedan diferidas a X3b — requieren extender el AST de Expr con un variant nuevo y tocar ~160 match arms. Procedures como statement-level routines son una pieza mucho más acotada que paga sola.

📝 Actualización 2026-06-15: X3b entregada por ADR-0032 el 2026-05-28. Funciones escalares ya son invocables desde SELECT/WHERE/CHECK/HAVING/UPDATE SET.

💡 Decisión

1. Sintaxis canónica

CREATE PROCEDURE name(p1 TYPE [, p2 TYPE]*) AS <body>;

DROP PROCEDURE [IF EXISTS] name;

CALL name(arg1, arg2, ...);

Donde:

2. Persistencia: ObjectKind::Procedure (VERSION 15)

El catálogo, que desde V14 tenía [kind:u8] con 0=Table, 1=View, 2=Trigger, suma 3=Procedure. Payload:

[name][param_count:u16] · param_count × ([param_name][type_code:u8]) · [body_sql]

VERSION bump 14 → 15 — V14 abierto por un binario X3+ rebota con [GBY-1003]. Migración manual: dump + recreate.

3. Substitución de parámetros: token-level con nombres bare

A diferencia de los triggers (donde NEW.x / OLD.x están cualificados y son inconfundibles), los parámetros de procedure son idents bare. Eso introduce una limitación conocida: si una columna real tiene el mismo nombre que un parámetro, el ident de la columna también se substituye y la query rompe.

Workaround documentado (mismo que PostgreSQL recomienda para evitar conflictos): usar prefijos en los nombres de parámetro:

-- ❌ choque: `id` aparece como param Y como columna de la tabla.
CREATE PROCEDURE add_log(id INT) AS INSERT INTO log (id) VALUES (id);
-- En el body, ambos `id` se substituirían — el del INSERT INTO log (id) se vuelve
-- INSERT INTO log (10) que falla como sintaxis.

-- ✅ prefijo
CREATE PROCEDURE add_log(p_id INT) AS INSERT INTO log (id) VALUES (p_id);

4. CALL es un statement standalone

CALL no se puede usar como Expr ni dentro de SELECT — es solo un statement top-level. Eso es coherente con la naturaleza side-effect-only de las procedures (no retornan valor). Para “función” invocable en SELECT hay que esperar X3b.

El executor:

  1. Lookup de la procedure por nombre → [GBY-4099] si no existe.
  2. Validar arity → [GBY-4100] si difiere.
  3. Evaluar cada arg con eval_expr_full contra fila vacía + outer scope nulo.
  4. Bind param_name (lowercased) → valor.
  5. Token-substitution sobre body_sql.
  6. parse(substituted)Vec<Statement> (split por ; interno).
  7. Exec cada statement en orden. Falla a mitad → propaga el error → wrap de transacción del caller hace rollback.

5. Type checking en CALL: best-effort

Hoy NO se valida que el tipo del arg matchee con el tipo declarado del param (e.g. CALL foo('text') cuando foo(p INT)). El motor confía en que el INSERT/UPDATE downstream rebote en runtime con un type mismatch. Validación estricta queda diferida.

6. No hay recursion guard (por ahora)

Una procedure que se llama a sí misma vía CALL desde su body NO está bloqueada por un depth counter — el wrap de transacción del caller eventualmente rebota por stack overflow (Rust recursion limit) o exhaustion de recursos. Si esto se vuelve problema, agregar un proc_depth similar al trigger_depth es trivial.

📐 Códigos de error

Código Nombre Cuándo
GBY-4097 PROCEDURE_NAME_COLLIDES Nombre colisiona con tabla / vista / trigger / procedure existente.
GBY-4098 PROCEDURE_BODY_INVALID Body no es DML/BEGIN/END, body vacío, param duplicado, BEGIN sin END matching.
GBY-4099 PROCEDURE_NOT_FOUND CALL o DROP PROCEDURE sobre nombre inexistente (sin IF EXISTS).
GBY-4100 PROCEDURE_ARITY_MISMATCH CALL recibió N args; procedure declara M.

🧪 Validación

Suite x3_* en tests/integration_test.rs (9 tests):

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

🔭 Futuro

Con X3, el bloque X queda con cobertura razonable para los 3 casos de uso más comunes de “lógica del lado servidor”:

Falta solo funciones (X3b) para tener el quartet operativo clásico. Después de eso, X4 es lenguaje procedural completo — área que probablemente requiere su propia spec de proyecto.