Estado: ✅ Aceptada Fecha: 2026-05-28 Bloque: W3 (tercer y último sub-bloque del bloque W del roadmap) Bump on-disk: ninguno (puro motor de proyección)

🧭 Contexto

Con W1 (CTEs no recursivas) y W2 (WITH RECURSIVE) entregadas, queda cerrar la pieza grande del bloque W: window functions. SQL las introduce con el operador OVER (PARTITION BY ... ORDER BY ...) aplicado a una función — el clásico ROW_NUMBER() OVER (PARTITION BY region ORDER BY salary DESC) para enumerar dentro de un grupo, SUM(amount) OVER (ORDER BY date) para totales corridos, LAG/LEAD para mirar fila anterior/siguiente, etc.

Es la primera vez que el motor proyecta valores que dependen de OTRAS filas (no sólo de la fila actual). Eso fuerza un pipeline distinto al del SELECT clásico.

💡 Decisión

1. Catálogo de funciones soportadas

Tres familias, todas en un solo push:

Familia Funciones Args ORDER BY
Ranking ROW_NUMBER, RANK, DENSE_RANK 0 opcional
Bucket NTILE(n) 1 requerido
Aggregate COUNT(*), COUNT(expr), SUM, AVG, MIN, MAX 0 o 1 opcional (cambia semántica del frame)
Value LAG(expr[, offset[, default]]), LEAD(...), FIRST_VALUE(expr), LAST_VALUE(expr) 1..3 LAG/LEAD requeridos

2. WindowSpec mínimo

OVER ( [PARTITION BY expr {, expr}*]
       [ORDER BY    expr [ASC|DESC] {, expr [ASC|DESC]}*] )

No soportamos frame specs explícitas (ROWS BETWEEN ... AND ..., RANGE, GROUPS) ni window naming (WINDOW w AS (...)). El default de frame se aplica según familia (ver §3).

3. Defaults de frame (sin spec explícita)

4. Arquitectura del pipeline

El executor detecta windows en stmt.columns y deriva a exec_window_select. Pipeline:

1. Validar: no GROUP BY/HAVING/aggregate clásico mezclado ([GBY-4090])
2. Validar arity y `ORDER BY` obligatorio por función
3. Clonar stmt con columns=[Star] + sin ORDER BY/LIMIT/OFFSET
4. Ejecutar via exec_select_query → ResultSet con TODAS las filas source
5. Convertir a Vec<HashMap<String, Value>> (con keys cualificadas + suffix)
6. Por cada window item:
     - Particionar índices por partition_by exprs (clave string-encoded)
     - Ordenar cada partition por order_by exprs
     - Computar window value per row → Vec<Value>
7. Proyectar fila a fila: Column lookup / Expression eval / Window lookup precomputado
8. Aplicar el ORDER BY/LIMIT/OFFSET original sobre la proyección

La materialización completa es el costo: O(N) memoria en el peor caso. Para SELECTs grandes esto va a doler — futura optimización por streaming queda fuera del scope inicial.

5. Mezcla con GROUP BY: explícitamente rechazada

-- ❌ [GBY-4090]
SELECT region, SUM(amount), ROW_NUMBER() OVER (ORDER BY region) FROM sales GROUP BY region;

Workaround: envolver el GROUP BY en una derived table y aplicar la window sobre el resultado.

-- ✅
SELECT region, total, ROW_NUMBER() OVER (ORDER BY total DESC) AS rk
FROM (SELECT region, SUM(amount) AS total FROM sales GROUP BY region) AS agg;

6. Window functions en otros contextos: no permitidas

Solo el SELECT list del SELECT top-level acepta windows. WHERE / HAVING / ORDER BY-expr-del-mismo-select / body de CTE recursive / CHECK constraints todos rechazan con [GBY-4091] (el parser las acepta y el executor las rebota cuando intenta evaluarlas fuera del windowing path).

📐 Códigos de error

Código Nombre Cuándo
GBY-4087 WINDOW_FUNCTION_UNKNOWN (Reservado para nombres no reconocidos — hoy el parser no se equivoca; futuro para extensibilidad.)
GBY-4088 WINDOW_REQUIRES_ORDER_BY LAG/LEAD/NTILE sin ORDER BY dentro del OVER. También: nombre de función always-window sin OVER.
GBY-4089 WINDOW_ARG_MISMATCH Arity incorrecta (ROW_NUMBER(x), LAG(a,b,c,d), etc.). DISTINCT dentro de window aggregate.
GBY-4090 WINDOW_NOT_ALLOWED_WITH_GROUP_BY Mezcla con GROUP BY / HAVING / agregados clásicos.
GBY-4091 WINDOW_NOT_ALLOWED_HERE Window en RETURNING, etc.

🧪 Validación

Suite w3_* en tests/integration_test.rs (12 tests):

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

🔭 Futuro

Con W3 cierra el bloque W completo — CTEs (W1+W2) y window functions (W3). Próximas piezas grandes son Fase 3 (planner + EXPLAIN + comparativa con SQLite/PG/MySQL/DuckDB) y los bloques X (triggers + stored procs), Y (tipos faltantes: DECIMAL/BLOB/UUID), Z (control de acceso).