Estado: ✅ Aceptada Fecha: 2026-05-08 Contexto que la motiva: auditoría de patrones de diseño para optimización de recursos (bloque 9 del roadmap interno) → commit c1316ea.

🧭 Contexto

Pre-bloque-9 todos los walks del B+Tree retornaban Vec<KeyValue> materializado:

// catalog.rs (pre-bloque-9)
pub fn scan_rows(&mut self, root: u32, offset: usize, limit: Option<usize>)
    -> DbResult<Vec<KeyValue>>
pub fn range_rows(&mut self, root: u32, from: i64, to: i64)
    -> DbResult<Vec<KeyValue>>

Esto es correcto para tablas pequeñas (los tests usan ~600 filas y nunca se nota), pero el costo crece linealmente con el tamaño de la tabla, independientemente del LIMIT pedido:

El bloque 7 (ORDER BY) agudizó el problema: cuando hay ORDER BY se setea defer_window = true, lo que forza materializar todo aunque haya LIMIT chico, porque el sort necesita el conjunto completo. Sin un mecanismo de iteración lazy, ninguna optimización de window puede convivir con ORDER BY.

Restricciones del proyecto:

💡 Decisión

Implementar bptree::LeafCursor<'a> que:

  1. Implementa Iterator<Item = DbResult<KeyValue>>.
  2. Anclado a un root del B+Tree, descenso al leaf inicial (leftmost para full scan, find_leaf(from) para range).
  3. Carga la página leaf actual en buf: Vec<KeyValue>, drena con pos: usize, salta a la siguiente leaf vía la chain next cuando se vacía.
  4. Mantiene upper: Option<i64> para corte temprano en range scans inclusive.
  5. Sticky done: bool para EOF idempotente.

Constructores públicos en Tree:

Wrappers en Catalog (consumen self para liberar el descenso del borrow del Pager antes de construir el cursor):

exec_select reescrito: cuando NO hay ORDER BY, los planes FullScan y Range consumen el cursor con .skip(stmt.offset).take(stmt.limit.unwrap_or(usize::MAX)).collect::<DbResult<Vec<_>>>()?. La promesa de Iterator::take (no avanza el inner iterator más allá del N-ésimo Some) es lo que vuelve la operación O(N + offset) en disco, no O(table_size).

🔄 Alternativas consideradas

Mantener Vec<KeyValue> y aceptar el costo

Cambiar API a for_each(callback) en lugar de Iterator

fn scan_with<F: FnMut(KeyValue) -> ControlFlow<()>>(&mut self, root: u32, f: F)
    -> DbResult<()>;

Iterator que internamente clona el Pager

Iterator pattern como aquí + &mut Pager exclusivo

📊 Consecuencias

Positivas

Negativas

Neutras

🔗 Referencias