Propósito: dejar registrado qué cambió, qué quedó frágil, qué características chocan entre sí, qué hay que reparar y qué hay que mejorar. Es un diagnóstico después de 12 pushes en 48h — el ritmo expuso tensiones que conviene nombrar antes de seguir construyendo.

Actualización 2026-06-11 (post-acciones del análisis)

Tras escribir el análisis original (justo después de P5e), seguimos trabajando sobre las tensiones identificadas. Las acciones aplicadas:

Marcador rápido al 2026-06-11: 5 de 9 tensiones del análisis original cerradas; 4 abiertas (2 cosméticas: 2.5, 2.6 · 2 dependientes de bench data: 2.3, 2.4 · 1 deuda documentada: 2.9). Suite tests 745 → 798.

Actualización 2026-06-15 (sesión maratón de cierre)

Diez pushes en una sesión cerraron 3 tensiones más y la mayoría de las reparaciones abiertas:

Marcador rápido al 2026-06-15: 7 de 9 tensiones cerradas. Quedan 2 abiertas: 2.6 (RIGHT/FULL JOIN heurística — documentada en ADR-0073, R10 la mitigó parcialmente) y 2.9 (P5d swap puede cambiar orden sin ORDER BY — deuda documentada; M3 la sortea ordenando en Rust). Suite tests 798 → 828 al cierre del día (15 pushes consecutivos). ADRs 0077 → 0090 (+13 nuevos).

Segunda ola 2026-06-15 (5 pushes adicionales) — endurecimiento y features SQL-estándar

Después de cerrar las reparaciones de la primera ola, la sesión siguió con:


1. Resumen de los cambios

Bloque Qué entregó VERSION Riesgo introducido
P3b (2026-06-09) stats por-tabla persistidas (ObjectKind::TableStats) 31→32 bajo
P4 (2026-06-10) stats por-columna (null/HLL/MCV/histograma) 32→33 medio (HLL precisó hot-fix splitmix64)
P5a (2026-06-11) estimate_selectivity + EXPLAIN est.match zero-bump nulo (annotation only)
P5b (2026-06-11) composite secondary index lookup (cierra Gap 10) zero-bump bajo
P5c (2026-06-11) cost-based skip-index si sel ≥ 0.2 zero-bump medio (primer plan-cambio por stats)
P5d (2026-06-11) hash-join build-side swap si current > right × 2 zero-bump bajo (cardinality real)
P5e (2026-06-11) EXPLAIN anota algoritmo real de JOIN zero-bump nulo (annotation)
R1 (2026-06-11) detección stats stale + bypass P5c zero-bump nulo (mitigación tensión 2.1)
R4 (2026-06-11) tests HLL sobre TEXT/UUID/DATE/DECIMAL zero-bump nulo (tests, mitigación tensión 2.8)
M2 (2026-06-11) gabybench smoke en CI + artifact JSON zero-bump nulo (CI)
R8 (2026-06-11) composite-eq fast-path para UPDATE/DELETE zero-bump bajo (reusa helpers P5b)
R6 (2026-06-11) post-lookup bucket-size check zero-bump bajo (mitigación tensiones 2.2/2.7)

Tests: 745 → 798 (+53). Pasaron en 5 runners de CI (ubuntu/macos/windows/docker + nuevo job bench).

Líneas tocadas: ~5 000 LOC en src/sql.rs, src/catalog.rs, src/storage.rs, src/bin/gabybench.rs, tests/integration_test.rs, .github/workflows/ci.yml, más 11 ADRs nuevos (0067–0077).


2. Tensiones y choques entre características

2.1 Stats stale silenciosas (heredado P3 → amplificado por P5c) — ✅ CERRADA por R1

Problema: ANALYZE TABLE corre una vez, las stats persisten. Pero si la tabla cambia mucho después, ni el motor ni EXPLAIN lo señalan. Pre-P5c era informativo nomás. Post-P5c es load-bearing: el plan cambia según stats. Una tabla con 1k filas analizadas hace tiempo, y ahora 1M filas → P5c puede elegir FullScan creyendo que est.match es razonable cuando en realidad escanea 1M filas para devolver 1.

Síntoma esperable: queries que iban rápido se ralentizan tras un crecimiento de tabla, sin error visible. El usuario no sabe que sus stats son obsoletas.

Magnitud: alto — es la principal externalidad de P5c.

2.2 Independencia asumida en AND (heredado P5a → propagado a P5c) — 🟡 PARCIALMENTE MITIGADA por R6

estimate_selectivity(And(a, b)) = sel(a) × sel(b). Asume columnas independientes. En la realidad están correlacionadas (marca='Toyota' AND modelo='Corolla' no es 0.01 × 0.001 — es más cerca de 0.001 porque modelo implica marca). P5c usa esa estimación para decidir FullScan vs index.

Síntoma esperable: en queries con WHERE a AND b muy correlacionadas, P5c sub-estima est.match y elige el path equivocado. Falla silenciosa.

Magnitud: medio — afecta solo a queries con AND correlacionado.

2.3 INDEX_BREAKEVEN = 0.2 sin calibración empírica — ✅ CERRADA por R2

El umbral viene del cost model teórico C_RANDOM ≈ 5 × C_SEQ. No está calibrado contra gabybench. Si en la práctica nuestro motor tiene C_RANDOM ≈ 2 × C_SEQ (SSD cache-friendly), el umbral correcto sería ~0.5. Estamos saltando al FullScan demasiado pronto.

Magnitud: medio. Requiere experimentación con gabybench → ajustar la constante.

2.4 Threshold de P5d (2×) sin medición — ✅ CERRADA por R3 + R3-cont

Mismo problema en menor escala. El swap del build-side se dispara a 2×. Pudo haber sido 1.5× o 3×. Sin gabybench JOIN-heavy no sabemos.

Magnitud: bajo. El cambio es semánticamente seguro; solo el threshold óptimo es desconocido.

2.5 Mensaje en EXPLAIN ambiguo entre P5c y “no hay índice” — ✅ MITIGADA por R7

SCAN `t` (P5c: hash-index `idx_cat` disponible
          pero stats prefieren FullScan + post-filter)

vs

SCAN `t` (full scan + WHERE cat=... post-filter; considerá `CREATE INDEX` sobre `cat`)

El usuario novato que ve “P5c skip-index” puede asumir que “no debería usar índice nunca”. El mensaje educativo de pre-P5c se silenció. Esto se nota más cuando un dev no conoce la lógica de break-even.

Magnitud: bajo — UX cosmético, no de correctness.

2.6 RIGHT/FULL JOIN en EXPLAIN — heurística imprecisa

P5e reporta hash join para todos los RIGHT/FULL JOIN, pero el dispatcher real puede usar nested-loop. Es imprecisión documentada (mejor que mentir con “nested-loop” siempre), pero deja una ventana donde EXPLAIN no coincide con la ejecución real.

Magnitud: bajo. RIGHT/FULL son raros en queries típicas.

2.7 Composite index NO-UNIQUE con bucket gigantesco — ✅ CERRADA por R6

Si el composite index (qty, precio) se usa sobre valores muy repetidos (p.ej. todas las filas con qty=5, precio=100), el bucket guarda miles de PKs. La lookup devuelve esos PKs, hace miles de random reads — exactamente el caso que P5c quiere evitar. Pero P5c NO se aplica a composite indexes (solo a single-col). El bench tipo “warehouse con valores skewed” sufriría.

Magnitud: medio. No es un bug; es un caso edge que P5c no cubre por diseño.

2.8 HLL bias en columnas con tipos no-INT — ✅ CERRADA por R4

El hot-fix de P4 (splitmix64 finalizer) se validó con INT secuenciales. Para DECIMAL, DATE, UUID, TEXT — no validado empíricamente. Si HLL está sesgado en algún tipo (ej. DECIMAL: el encoding incluye value + scale → distribución no uniforme), P5c puede tomar decisiones malas en queries sobre esos tipos.

Magnitud: medio. Específico a workloads con esos tipos.

2.9 P5d swap puede cambiar orden de resultado sin ORDER BY

Tests existentes (no en P5d) pueden asumir orden determinístico. P5d swap cambia el iterador exterior → orden ligeramente distinto. Threshold 2× minimiza casos, pero NO los elimina. Si un test futuro depende de orden en un JOIN sin ORDER BY, puede romperse al cruzar el 2× con datos nuevos.

Magnitud: bajo. Tests actuales pasan, pero es deuda potencial.


3. Lista de reparaciones (lo que se rompió o quedó débil)

# Item Severidad Estado
R1 Detección de stats stale en EXPLAIN + warning si P5c usa stats > X días vieja alta ✅ entregada (ADR-0074)
R2 Calibrar INDEX_BREAKEVEN_SELECTIVITY contra gabybench real alta ✅ entregada (ADR-0081): 0.20 → 0.10
R3 Calibrar threshold P5d (1.5× vs 2× vs 3×) media ✅ cerrada con outcome — instrumentación (ADR-0082) + sweep empírico inconcluso (ADR-0085): default 2.0 stays
R4 HLL test sobre DECIMAL/DATE/UUID/TEXT media ✅ entregada (sin ADR; ver r4_* tests)
R5 TRUNCATE TABLE limpia stats persistidas media 🔴 abierto (bloqueado por implementar TRUNCATE como statement)
R6 Composite index lookup con bucket gigantesco — refinamiento de P5c media ✅ entregada (ADR-0077)
R7 Mensaje EXPLAIN del P5c skip — sugerir re-ANALYZE si stats viejas baja ✅ entregada (ADR-0078)
R8 UPDATE/DELETE con composite-eq aún hacen FullScan media ✅ entregada (ADR-0076)
R9 COUNT(DISTINCT col) sobre JOIN — ADR-0066 Gap 1 residual baja ✅ entregada (ADR-0079)
R10 USING/NATURAL JOIN — EXPLAIN heurística conservadora baja ✅ entregada (ADR-0080)

6 de 10 cerradas. Las 4 abiertas restantes están detalladas en TAREAS_PENDIENTES.md §4 con esfuerzo estimado.


4. Lista de mejoras (lo que aún no se rompió pero falta)

4.1 Fundacionales (la base que falta antes de seguir)

# Item Por qué importa
M1 Auto-ANALYZE (scheduler que dispare cuando la tabla cambió > X%) Hoy las stats son responsabilidad manual del usuario. Sin esto, R1 (stale detection) es un workaround.
M2 gabybench reproducible en CI con tracking de regresiones (el P6 declarado en el plan) Sin esto no podemos calibrar R2/R3 ni detectar regresiones de P5c/P5d en futuros bloques.
M3 Property tests para JOIN reorder y P5c (proptest sobre planes A vs B sobre los mismos datos) Validar que cambios futuros del optimizer no introducen regresiones de correctness silenciosas.
M4 Fuzz testing del parser (cargo fuzz) — declarado en TAREAS_PENDIENTES como prioridad alta Una hora limpia de fuzz es línea creíble en README; cero no lo es.

4.2 Planner y stats (continuación natural)

# Item Por qué importa
M5 Multi-column stats (correlación inter-columna) — CREATE STATISTICS (col_a, col_b) Resolvería 2.2 (independencia AND). PostgreSQL lo tiene desde 10. Aquí: ~600 LOC nuevos + bump VERSION.
M6 EXPLAIN ANALYZE compara est.match vs actual=K Diagnóstico directo de sesgos del estimator sin tener que correr queries separadas.
M7 Hints SQL (/*+ INDEX(t, idx_cat) */, /*+ HASH_JOIN(a, b) */) Override per-query del cost model cuando los stats fallan. Estándar en Oracle/MySQL/SQL Server.
M8 Prefix matching sobre composite index (P5b-futuro / P5c-futuro) WHERE a=X con INDEX (a, b) — requiere cambio de layout on-disk a lexicographic tuple-bytes. Bump VERSION.
M9 Base table reorder para INNER chains (extensión natural de P5d) Hoy P5d swap solo el step current. Reorder global requeriría refactor de build_join_scope.

4.3 Resiliencia y operación

# Item Por qué importa
M10 Detectar stats stale con analyzed_at_nanos ya persistido Ya tenemos el dato; falta consumirlo. EXPLAIN podría mostrar [stats hace 7d 5h] y P5c bajarse cuando son muy viejas.
M11 WAL-mode opt-in (ADR-0018 pendiente, archivado por riesgo) Habilitaría lectores concurrentes, abriría espacio comparativo serio vs SQLite.
M12 SAVEPOINT + ROLLBACK TO SAVEPOINT (T1 declarado en STATUS pendientes) Hoy ROLLBACK descarta todo el batch — no se puede recuperar parcialmente de errores.
M13 Cross-request transactions en server HTTP (T2) El server actual recrea transacción por request. Sin esto, no se puede escribir un cliente que haga BEGIN/INSERT/INSERT/COMMIT.

4.4 Calidad de código y DX

# Item Por qué importa
M14 Refactor de exec_select_with_where (~2 000 LOC en una función) Se está volviendo difícil de razonar. Extraer plan-selection a un struct/módulo aparte.
M15 #[doc(hidden)] o módulo privado para helpers de P5* estimate_selectivity, find_matching_composite_index, etc. están en el top-level de sql.rs. Conviene encapsularlos.
M16 Tests de regresión empíricos — guardar est.match esperado vs el que devuelve el motor para 20+ queries representativas, fallar si la diferencia supera X% Detecta regresiones de calibración sin tener que correr el bench entero.

5. Análisis de riesgo de la sesión completa

Qué salió bien

Qué salió frágil

Qué pasaría si se aplica más optimizer sin las reparaciones

Sumar P5f/P5g (reorder global de joins, estimación cross-JOIN) sobre las limitaciones actuales amplifica las decisiones malas. Si P5c ya puede equivocarse por stats stale o correlación, P5f equivocaría JOINs enteros.

Conclusión operativa: antes de seguir agregando optimizer, hacer R1, R2, M2 (gabybench en CI) en algún orden. Esto da la red de seguridad que faltó durante esta sesión.


6. Recomendación de orden — actualizada 2026-06-15

Original (antes de hacer nada): M2 → R1 → R2 → R4 → M4 → R5/R8/R9.

Lo que efectivamente se hizo en la sesión 2026-06-11: R1 → R4 → M2 → R8 → R6.

Lo que se hizo en la sesión maratón 2026-06-15 (10 pushes en orden): R7 → R9 → R10 → R2 → R3 → bench-fix → ANSI-fix → M3 → R3-cont → docs sweep.

Diferencias con la recomendación previa:

Próximo orden recomendado desde 2026-06-15 (cuando vuelvas):

  1. M4 — cargo fuzz sobre el parser. 1 hora limpia es la línea de README que sigue faltando. Único bloqueo: setup inicial de cargo-fuzz.
  2. M6 — EXPLAIN ANALYZE compara est.match vs actual=K. Diagnóstico directo del bias del estimator. Hoy ANALYZE re-ejecuta; solo falta agregar la columna comparativa. ~200 LOC.
  3. Proptest sobre Pager (no sobre planner, eso es M3 ya cerrada). Misma técnica zero-deps que M3 — defiende correctness de begin/insert/commit/rollback bajo input adversarial.
  4. M12 — SAVEPOINT + ROLLBACK TO SAVEPOINT. Desbloquea M13 (cross-request tx en server HTTP).
  5. Demo pública de Fase 5 (MCP gateway + vector search + audit). La palanca real para que el repo “sea visto”.

Lo que NO conviene hacer antes: M5 (multi-col stats) y M9 (JOIN reorder). Sumar más optimizer sin sostener M4 (fuzz) → construir sobre arena. M11 (WAL-mode) es Fase 6.

Otra opción razonable: parar acá. Las 7 de 9 tensiones cerradas, la suite es 828/828 verde, gabybench all 71/71 queries sin SKIPs, 3 redes property-based defienden el optimizer contra regresiones futuras, ANSI fix elimina la asimetría más visible con clientes portados. Es un punto de descanso natural con el motor en mejor estado que nunca antes. El siguiente eje (más optimizer / más DDL / más ergonomía / demo pública) puede esperar a contexto externo (tweet, proyecto comparativo activo, etc).