Full-scan decode — the read_all fast path
Class:
design· Function:read· Uses:read.rs,warehouse,znippy-zoomies/gatling_forkjoin,parquet. Only the shape that runs. This is howTable::read(read_all→read_all_gatling) turns a plain append-only snapshot into Arrow batches without paying the Iceberg engine's per-file asyncto_arrowpipeline — and, as of the local-fd change, without slurping the whole dataset into memory.
The three-way decode
read_all picks the cheapest correct decode for the current snapshot:
- Fast gatling path (
read_all_gatling, the common case) — a plain append-only, single-schema table with no delete files. The data-file set is resolved from the manifest list, then decoded straight to Arrow across all cores via the ONE fork-join engine (gatling), one worker per file, batches kept in file order. Bypasses the engine's per-file asyncto_arrow. - Engine fallback — schema evolution (field-id remap) or any delete file (row-level merge-on-read).
read_all_gatlingreturnsNoneandread_allfalls back toscan().select_all().to_arrow(). Correctness first: the fast path never drops a deleted row or mis-maps an evolved schema. - Empty — no current snapshot ⇒ zero batches.
Inside the fast path: local-fd streaming vs whole-Bytes slurp
Once the data-file set is known, the decode itself has two byte/row-identical strategies — same Parquet reader, same default batch size, same per-file gatling fan-out, same file order — chosen by whether the FileIO backend is local:
read_all_gatling
├─ resolve data files (walk manifest list + parse manifests)
├─ all_local_paths(files)? # every file a file:// / absolute path that exists
│ ├─ Some(paths) → decode_data_local(paths) # LOCAL — stream from fd
│ └─ None → decode_data_bytes(file_io, …) # REMOTE — whole-Bytes via FileIO
└─ batches (file order)
decode_data_local(local warehouse — the commonWarehouse::opencase, and the oxr/mashup read). Each file is opened as astd::fs::Fileand decoded straight from the fd — the Parquet reader pulls pages lazily. Nothing is pre-slurped. Peak memory is the decoded batches + per-file page buffers, and each file's I/O overlaps its own decode.decode_data_bytes(any non-local FileIO — S3, memory). The backend-neutral fallback: read each file's whole bytes through the Iceberg FileIO, then decode from the in-memoryBytes. This is what every backend can serve.
The gate is all-or-nothing (all_local_paths returns None if any file isn't a local file that exists), so a single scan never mixes the two strategies.
Why the local path exists (the waste it removes)
The pre-change fast path did decode_data_bytes for every backend: it read all data files' whole bytes into a Vec<Bytes> before decoding a single row — so peak RSS was the entire dataset, and the I/O was a serial pre-read in front of the decode. Against raw Parquet (which streams pages lazily from the fd) this both lost throughput and spiked memory on a full scan of a big local table. For a local warehouse — where the files are right there on disk — reading whole bytes into the heap first is pure waste. decode_data_local removes it while staying byte/row-identical to the fallback, so the S3 path is untouched.
The data-file resolution (manifest-list walk + Avro manifest parse) is inherent — a full scan must learn which files the snapshot points at — and is unchanged.
Correctness / equivalence
The two decoders emit the identical batch sequence (schema + values + order), guarded RED-when-broken by local_streamed_decode_is_byte_identical_to_bytes (streamed decode == the Bytes reference over a multi-file, multi-row-group fixture; reversing the file order fails it) and read_all_gatling_uses_local_streamed_path (a local warehouse takes the streamed path — LOCAL_STREAMED_READS counter — with no Bytes-slurp fallback, all rows intact). The read_all ↔ engine-fallback equivalence is covered by the existing roundtrip / data-skip suites.
Reuse (L5)
The file:///absolute → local-path rule is warehouse::local_path_of (pub(crate)), consumed by both the compaction data-dir logic and this read fast path — never re-derived. The all-core decode is the ONE fork-join engine (gatling), the same one read_delta / read_row_groups use — no private thread pool.