vibesql-executor 0.2.0

Query execution engine for vibesql SQL database
Documentation
# Iterator-Based Query Execution

This module implements lazy iterator-based query execution for SQL SELECT statements. It provides significant performance improvements for queries that can avoid full materialization.

## Overview

Traditional SQL execution materializes all intermediate results into memory before processing the next stage. Iterator-based execution streams rows on-demand, only computing what's needed.

**Key Benefits**:
- **Memory Efficiency**: O(LIMIT) instead of O(N) memory usage
- **Early Termination**: `LIMIT 10` on 1M rows processes ~10 rows, not 1M
- **Lazy Evaluation**: WHERE filtering and OFFSET happen during iteration
- **Performance**: 10-100x faster for queries with LIMIT on large tables

## Architecture

### Core Trait: `RowIterator`

```rust
pub trait RowIterator: Iterator<Item = Result<storage::Row, ExecutorError>> {
    fn schema(&self) -> &CombinedSchema;
    fn row_size_hint(&self) -> (usize, Option<usize>);
}
```

All iterator types implement this trait, enabling uniform composition.

### Iterator Types

#### 1. **TableScanIterator**
Wraps materialized `Vec<Row>` for uniform iteration.

```rust
let scanner = TableScanIterator::new(schema, rows);
```

**Purpose**: Adapts materialized rows to iterator interface.
**Memory**: O(N) - rows already materialized from storage.

#### 2. **FilterIterator**
Lazily evaluates WHERE predicates during iteration.

```rust
let filter = FilterIterator::new(source, predicate, evaluator);
```

**Purpose**: Applies WHERE clause without pre-filtering all rows.
**Memory**: O(1) per row - no intermediate storage.
**Benefit**: Combined with LIMIT, only evaluates needed rows.

#### 3. **ProjectionIterator**
Transforms rows using a projection function.

```rust
let project = ProjectionIterator::new(source, projected_schema, |row| {
    // Transform row values
    Ok(transformed_row)
});
```

**Purpose**: Column selection and expression evaluation.
**Note**: Currently not used in production (projection happens after materialization).

#### 4. **LazyNestedLoopJoin**
Streams left side while materializing right side.

```rust
let join = LazyNestedLoopJoin::new(
    left_iter,
    right_schema,
    right_rows,
    join_type,
    condition,
    evaluator,
)?;
```

**Purpose**: Enables lazy join execution for large left sides.
**Memory**: O(left_current + right_all) vs O(left_all * right_all).
**Limitation**: Right side must be materialized (nested loop algorithm).

## Execution Pipeline

### Iterator Path (Simple Queries)

```
scan → filter → skip → take → collect → project
 ↓       ↓       ↓      ↓       ↓         ↓
FROM   WHERE   OFFSET LIMIT    Mat.    SELECT
```

**When Used**: Queries without ORDER BY, DISTINCT, or window functions.

**Example**:
```sql
SELECT * FROM users WHERE age > 18 LIMIT 10 OFFSET 100;
```

**Pipeline**:
1. **Scan**: Stream rows from `users` table
2. **Filter**: Evaluate `age > 18` lazily
3. **Skip**: Skip first 100 matching rows (lazy)
4. **Take**: Stop after 10 rows (early termination)
5. **Collect**: Materialize final 10 rows
6. **Project**: Apply SELECT list to materialized rows

**Performance**: Processes ~110 rows instead of entire table.

### Materialized Path (Complex Queries)

```
FROM → WHERE → Sort/Distinct/Windows → SELECT → LIMIT/OFFSET
 ↓       ↓              ↓                  ↓         ↓
Mat.   Filter        Process             Project   Slice
```

**When Used**: Queries with ORDER BY, DISTINCT, or window functions.

**Example**:
```sql
SELECT * FROM users ORDER BY age LIMIT 10;
```

**Why Materialized**: Sorting requires all rows to determine top 10.

## Decision Logic

The `can_use_iterator_execution()` function determines execution path:

```rust
fn can_use_iterator_execution(stmt: &ast::SelectStmt) -> bool {
    // Can't use iterators if we have ORDER BY (requires sorting all rows)
    if stmt.order_by.is_some() {
        return false;
    }
    // Can't use iterators if we have DISTINCT (requires deduplication)
    if stmt.distinct {
        return false;
    }
    // Can't use iterators if we have window functions
    if has_window_functions(&stmt.select_list) {
        return false;
    }
    true
}
```

## Performance Characteristics

### Memory Usage

| Query Pattern | Materialized | Iterator | Improvement |
|--------------|--------------|----------|-------------|
| `SELECT * FROM t LIMIT 10` | O(N) | O(10) | 100x for N=1000 |
| `SELECT * FROM t WHERE ... LIMIT 10` | O(N) | O(k) | k ≈ rows scanned until 10 found |
| `SELECT * FROM t OFFSET 500 LIMIT 10` | O(N) | O(510) | 2x for N=1000 |
| `SELECT * FROM t ORDER BY c LIMIT 10` | O(N) | O(N) | No benefit (must sort) |

### Time Complexity

**Iterator Path**:
- Best Case: O(LIMIT) - all matching rows at start
- Average Case: O(N/2) - matching rows distributed
- Worst Case: O(N) - few/no matching rows

**Materialized Path**: Always O(N)

**Key Insight**: Iterator path degrades gracefully to O(N), never worse than materialized.

## Design Decisions

### 1. Projection After Materialization

**Current**: Project after collecting filtered rows.
**Alternative**: Lazy projection in iterator pipeline.

**Rationale**:
- SQL projection is complex (wildcards, expressions, aliases)
- Computing output schema requires AST analysis
- Key benefits (early termination, lazy filtering) preserved
- Simpler implementation reduces bugs

### 2. Right Side Materialization in Joins

**Current**: LazyNestedLoopJoin materializes right side.
**Alternative**: True streaming joins (hash join, merge join).

**Rationale**:
- Nested loop joins inherently scan right side multiple times
- True streaming requires different join algorithms
- Still provides benefit for large left sides
- Pragmatic compromise for initial implementation

### 3. CSE Cache Management

**Issue**: Common Sub-Expression Elimination (CSE) cache was caching column references.
**Impact**: First row's values cached and reused for all rows.
**Fix**: Mark `ColumnRef` as non-deterministic in `expression_hash.rs`.

```rust
// Literals are deterministic, but column references are NOT
// Column references depend on the current row data
ast::Expression::Literal(_) => true,
ast::Expression::ColumnRef { .. } => false, // Fixed!
```

**See**: `crates/executor/src/evaluator/expression_hash.rs:138`

## Testing

### Unit Tests

Located in `crates/executor/src/select/iterator.rs`:

```bash
cargo test --package executor --lib iterator::tests
```

**Coverage**:
- TableScanIterator: empty and non-empty
- FilterIterator: truthy/falsy values, column references
- LazyNestedLoopJoin: all join types, early termination
- Pipeline composition: scan → filter → take
- Phase C proof-of-concept: full end-to-end pipeline

### Integration Tests

All 589 executor tests validate correctness:

```bash
cargo test --package executor
```

### Benchmarks

Quantify performance improvements:

```bash
cargo bench --package executor --bench iterator_execution
```

**Benchmark Groups**:
1. `limit_early_termination` - LIMIT with/without
2. `where_with_limit` - Lazy filtering + early termination
3. `offset_with_limit` - Lazy skip optimization
4. `projection_with_limit` - Projection cost amortization
5. `materialized_queries` - Baseline (ORDER BY, DISTINCT)

## Future Enhancements

### 1. Lazy FROM Clause

**Goal**: Make `execute_from()` return iterators instead of `Vec<Row>`.

**Benefits**:
- Enable lazy table scans
- Stream through single-table queries
- Reduce memory for large tables

**Challenge**: Requires refactoring join execution and predicate pushdown.

### 2. Hash Join Iterator

**Goal**: Implement hash join with streaming left side.

**Benefits**:
- O(1) lookup for right side (vs O(M) nested loop)
- Maintain lazy left side streaming
- Better performance for large joins

**API**:
```rust
pub struct HashJoinIterator<L: RowIterator> {
    left: L,
    right_hash_table: HashMap<Vec<SqlValue>, Vec<Row>>,
    // ...
}
```

### 3. Lazy Projection

**Goal**: Move projection into iterator pipeline.

**Benefits**:
- Project only LIMIT rows, not all filtered rows
- Reduce memory for wide tables with narrow projections

**Challenge**:
- Computing output schema from AST
- Handling wildcard expansion
- Managing column aliases

### 4. Parallel Iterators

**Goal**: Use rayon for parallel WHERE evaluation.

**Benefits**:
- Utilize multiple cores for filtering
- Faster predicate evaluation on large tables

**Challenge**:
- Thread-safe evaluator
- Work-stealing with LIMIT

## Migration Guide

### Adding New Iterator Type

1. **Define struct** with source iterator + transformation state
2. **Implement Iterator** with `next()` logic
3. **Implement RowIterator** for schema access
4. **Add tests** covering edge cases
5. **Update documentation** with usage examples

**Example Template**:
```rust
pub struct MyIterator<I: RowIterator> {
    source: I,
    // transformation state
}

impl<I: RowIterator> Iterator for MyIterator<I> {
    type Item = Result<storage::Row, ExecutorError>;

    fn next(&mut self) -> Option<Self::Item> {
        // Transformation logic
    }
}

impl<I: RowIterator> RowIterator for MyIterator<I> {
    fn schema(&self) -> &CombinedSchema {
        &self.output_schema
    }
}
```

### Expanding Iterator Coverage

To enable iterator execution for more query patterns:

1. **Identify pattern** currently using materialized path
2. **Implement iterator** for that pattern
3. **Update `can_use_iterator_execution()`** to include pattern
4. **Add tests** validating correctness
5. **Add benchmarks** measuring improvement

## References

- **Main Implementation**: `crates/executor/src/select/iterator.rs`
- **Integration Point**: `crates/executor/src/select/executor/nonagg.rs`
- **Benchmarks**: `crates/executor/benches/iterator_execution.rs`
- **CSE Fix**: `crates/executor/src/evaluator/expression_hash.rs`
- **Related Issue**: [#1123]https://github.com/rjwalters/vibesql/issues/1123

## Contributing

When working with iterator execution:

1. **Maintain correctness** - All 589 tests must pass
2. **Measure performance** - Add benchmarks for new patterns
3. **Document decisions** - Update this README with rationale
4. **Test edge cases** - Empty tables, NULL values, type mismatches
5. **Consider memory** - Profile memory usage for large datasets

---

**Last Updated**: 2025-11-09
**Author**: Issue #1123 Implementation
**Status**: Production-ready for queries without ORDER BY/DISTINCT/windows