Build your own mini columnar query engine · Stage 1 of 8
Scan Parquet in row-group batches
You are building a small columnar query engine in Python, with Apache Arrow
record batches as the in-memory format and DuckDB as the correctness oracle. You
grow a single file, engine.py, across all 8 stages. This stage lays the
foundation: reading a Parquet file one row group at a time.
The public API (stable for every stage)
All stages extend this same surface. Do not rename these names — the tests call them directly.
# Entry points
execute(plan) -> pyarrow.Table # run a plan tree, return the result
explain(plan) -> str # a Spark-like, indented plan string
new_scan(path, columns=None, pushdown=None) -> Scan # Scan factory (+ ScanStats)
# Plan nodes (frozen dataclasses) — a plan is a tree of these
Scan(path, columns=None, pushdown=None, stats=ScanStats)
Project(child, columns) # keep/reorder columns (stage 2)
Filter(child, predicate) # row filter, Volcano model (stage 2)
Aggregate(child, keys, aggs) # hash aggregate (stage 4)
HashJoin(left, right, left_keys, right_keys, how="inner") # stage 5
BroadcastJoin(...) # same fields as HashJoin (stage 5)
Exchange(child, keys, num_partitions) # hash-partition exchange (stage 6)
Optimized(child) # optional optimizer marker (stage 7)
# Expressions (used by Filter.predicate)
Col(name) / Lit(value)
Compare(left, op, right) # op in {"=","!=","<","<=",">",">="}
And(*preds) / Or(*preds)
The dataclasses are already defined in the starter. Your job is to implement the
behavior behind execute() and explain().
This stage's task
Implement scanning so that execute(new_scan(path)) returns the whole Parquet
file as a pyarrow.Table:
- Read the file row group by row group (
pyarrow.parquet.ParquetFile+read_row_group), yieldingRecordBatches — the beginning of a Volcano (pull-based) operator pipeline. - Honour column projection:
new_scan(path, columns=[...])reads only those columns. explain(plan)returns a string whose Scan line containsScan.- An empty Parquet file returns a 0-row table with the correct schema.
Constraints
- Arrow-native: results are
pyarrow.Tables. No pandas required. - Keep it lazy/iterator-based — later stages add operators that pull batches from
this scan, so structure
execute()around a per-node batch generator. - The suite runs in well under 60 s. Oracle: DuckDB
read_parquet(...).
Run it
de start SPK-BYO/01 # copies the cumulative starter into your workspace
de test # runs this stage (and, later, earlier stages too)
de hint # progressive hints
Hints
Hint 1
A Parquet file is a list of row groups. Open it with pyarrow.parquet.ParquetFile and loop over range(pf.num_row_groups), reading each with read_row_group(rg, columns=...).
Hint 2
execute() should drive a Volcano-style generator that yields pyarrow.RecordBatches, then assemble them with pa.Table.from_batches(...). Honour new_scan(columns=...) by passing columns to the reader.
Hint 3
For explain(), return a one-line string per node; the Scan line must contain the word 'Scan'. Build the empty-table case from the Parquet schema so an empty file still returns 0 rows with the right columns.
uv run de start SPK-BYO/01-scan
uv run de test