The plan is consumed in waves: the next wave file scan tasks go
through TableScan._read_files together, their batches are handed out in
task order, and only when the last of them has been yielded is the next
wave started. Nothing beyond the wave in flight is held, so a fold over a
scan costs one wave rather than one result — with the default
num_workers = 1 a wave is one data file.
Ordering
Rows arrive in exactly the order to_batches returns them, for every
num_workers. That is the whole reason for the wave: within a wave the
merge is _read_files' existing merge by task index, and waves are
consumed in plan order, so thread scheduling cannot reach the output. The
alternative — yielding each file as its worker finishes — would have made
a scan's row order depend on which core won a race, and therefore differ
between two runs of the same query on the same data. The num_workers
docstring promises the opposite, and to_table/to_batches both depend
on it; a streaming path that quietly broke it would be a worse API than
one that buffers.
The wave is TableScan._worker_split's file-axis width, which is
min(num_workers, len(tasks)), so every worker still gets a file and the
parallel path is as wide as it was. What the wave costs instead is a
barrier at its end: a wave is only done when its slowest file is, and a
worker that finishes early idles until then, where to_batches would have
moved it onto the next file. That is the price of the ordering guarantee,
and it is paid in time, not in memory.
A scan with a limit streams one file at a time — the file axis is not
used at all, exactly as in to_batches, because stopping early is only
meaningful in order.
Errors
next_batch is the primitive and raises what a read raises. batches()
exists for for batch in reader.batches():, and a for loop cannot carry
an error out: Iterator.__next__ may raise only StopIteration. A read
that fails inside the loop therefore ends the loop and parks the message
on the reader, and the caller must call raise_if_failed afterwards
or a truncated scan reads as a complete one:
var reader = table.scan().to_batch_reader(options)
var rows = 0
for batch in reader.batches():
rows += batch.num_rows
reader.raise_if_failed()