Fecha: 2026-05-29 Estado: Aceptado Bloque: P2 (Fase 3 — Performance / Planeación) Antecede: ADR-0063 (P1 — EXPLAIN)

Contexto

P1 introdujo EXPLAIN <stmt> como dry-run: devuelve un plan estimado (scan type, joins, filtros, order) sin ejecutar el statement. Útil para ver qué piensa hacer el engine, pero no responde la pregunta clave “¿cuánto realmente tardó y cuántas filas tocó?”.

P2 cierra ese gap agregando la modalidad EXPLAIN ANALYZE <stmt>: mismo plan que P1, más ejecución real del statement, más tiempo wall-clock medido con std::time::Instant, más conteo real de filas producidas.

Decisión

AST

Statement::Explain pasa de variante tupla a struct con flag:

Explain {
    analyze: bool,
    inner: Box<Statement>,
}

Parser

if self.match_keyword("EXPLAIN") {
    let analyze = self.match_keyword("ANALYZE");
    let inner = self.parse_statement()?;
    return Ok(Statement::Explain { analyze, inner: Box::new(inner) });
}

Engine — exec_explain(inner, analyze)

  1. Si analyze=true, clona inner antes del match plan-walker (el walker consume el inner original).
  2. Corre el walker P1 → llena steps con el plan estimado.
  3. Si analyze=true:
    • t_start = Instant::now()
    • exec_res = self.exec(inner_clone)
    • elapsed = t_start.elapsed()
    • Si Ok(rs): agrega actual.time (ms con 3 decimales) y actual.rows (conteo real, con sufijo de mensaje del inner si lo trae).
    • Si Err(e): agrega actual.error con código + ms.
  4. message del ResultSet diferencia:
    • Sin ANALYZE: "EXPLAIN: plan estimado (sin ejecutar el statement)".
    • Con ANALYZE OK: "EXPLAIN ANALYZE: plan + ejecución real (X ms, N rows). Cuidado: side-effects PERSISTIDOS.".
    • Con ANALYZE ERR: "EXPLAIN ANALYZE: plan + ejecución real (X ms, ERROR).".

Side-effects

ANALYZE ejecuta el statement real. Eso significa:

Si el usuario quiere dry-run, debe usar EXPLAIN <stmt> (sin ANALYZE). El warning en message lo deja explícito.

Error capture

Si el inner falla (PK violation, RLS check, syntax mid-exec, lo que sea), P2 no propaga el error como fallo de EXPLAIN ANALYZE. Lo captura como step actual.error y devuelve ResultSet OK con el plan + el error registrado como dato. Razón: EXPLAIN ANALYZE es una herramienta de observabilidad; el caller quiere ver “esto falló y tardó X ms”, no recibir un Err que descarta el plan.

Alternativas consideradas

  1. Hacer ANALYZE transaccional con rollback automático (PostgreSQL ofrece EXPLAIN (ANALYZE) BEGIN; ... ROLLBACK). Descartado: gabysql aún no expone transacciones explícitas como primer-clase. Sería una abstracción nueva. Lo dejo para Fase 4 (TX).

    📝 Actualización 2026-06-15: gabysql SÍ expone transacciones explícitas desde el bloque T (BEGIN/COMMIT/ROLLBACK, 2026-05-25)

    • M12 SAVEPOINT (ADR-0089) + M13 cross-request sessions (ADR-0090). El descarte de “EXPLAIN ANALYZE con rollback automático” sigue válido por la segunda razón (sería una abstracción que cambia semántica del EXPLAIN ANALYZE) — no por la primera.
  2. Reportar tiempo por sub-step (per-scan timing). Descartado para P2: requiere instrumentar cada loop interno (exec_select, exec_join, exec_filter, exec_order). Demasiado scope para un bloque. Queda para P4 (instrumentación granular).
  3. Hacer ANALYZE devolver dos result sets (plan + filas reales). Descartado: rompe contrato de ResultSet único por statement en el resto del engine.

Tests

Siete tests p2_* + uno renombrado del P1 obsoleto:

Consecuencias

Limitaciones / Trabajo futuro