3.7 KiB
Sprint 2: Streaming Parser
🏁 MVP Sprint 2: Streaming Parser Implementation
Goal: Refactor src/parser.rs to parse the Postgres binary stream as it arrives, rather than buffering the entire 130MB+ result set into a Vec<u8> first. This will significantly reduce memory overhead and improve processing latency.
Phase 1: Refactor handle_binary_copy State
This phase involves changing the structure of the function to support streaming.
-
Task 1: Initialize Builders First
- In
handle_binary_copy, move thebuilders: Vec<DynamicBuilder>creation to the top of the function, before the stream reading loop.
- In
-
Task 2: Initialize State Variables
-
Create a
leftover_buffer: Vec<u8> = Vec::new();to hold partial row data between chunks. -
Create a
is_header_parsed: bool = false;flag. -
Create a
rows_processed: usize = 0;counter.
-
-
Task 3: Refactor Finalization
- Move the "Finalize the Arrays" and "Build the Final RecordBatch" logic (Steps 4 and 5 in the old parser) to the very end of the function, after the main stream loop.
Phase 2: Implement the Streaming Loop
This phase rewrites the core while let Some... loop to parse in-place.
-
Task 4: Prepare the Chunk Buffer
-
Inside the
while let Some(segment_result) = stream.next().awaitloop: -
Create a new
current_chunk: Vec<u8>by combiningleftover_buffer+ the newsegment(fromsegment.extend_from_slice(...)). -
Clear the
leftover_buffer(leftover_buffer.clear()).
-
-
Task 5: Create a Cursor for the Chunk
- Create a
cursor = Cursor::new(¤t_chunk).
- Create a
-
Task 6: Handle the Header (Once)
-
Add an
if !is_header_parsedblock. -
Inside this block, parse the 19-byte Postgres header (magic signature, flags) using the
cursor. -
If the header is incomplete (hits
UnexpectedEof), copy the entirecurrent_chunkback intoleftover_bufferandcontinueto the next segment. -
If successful, set
is_header_parsed = true.
-
Phase 3: Implement the Inner Parsing Loop (The Core Logic)
This is the most complex part: parsing rows until the chunk runs out of data.
-
Task 7: Create the Inner
loop- Inside the
whileloop (after the header check), create a new, innerloop { ... }. This loop will repeatedly try to parse rows from thecursor.
- Inside the
-
Task 8: Implement Safe Read (Handling Partial Data)
-
Before attempting to read a row, save the current position:
let safe_position = cursor.position();. -
Attempt to read the row's 2-byte column count (
read_i16). -
If it fails with
UnexpectedEof:-
This chunk is finished, but the data is partial.
-
Copy the remaining bytes from
safe_positionto the end ofcurrent_chunkintoleftover_buffer. -
break;the inner loop (to go get the next network segment).
-
-
If it succeeds:
-
Check for the
-1trailer (end of stream). If found,breakboth loops. -
Increment
rows_processed.
-
-
-
Task 9: Implement Streaming Field Parsing
-
Inside the
for (i, builder) in builders.iter_mut().enumerate()loop: -
Save the position again (
let field_safe_pos = cursor.position();). -
Attempt to read the 4-byte field length.
-
Attempt to read the field data (e.g.,
read_i64). -
If any read fails with
UnexpectedEof:-
The row is partial and split across chunks.
-
Restore the cursor:
cursor.set_position(safe_position);(rewind to the start of the row). -
Copy all remaining bytes from
safe_positionintoleftover_buffer. -
break;the inner loop.
-
-
If it succeeds:
- Append the value to the correct Arrow builder.
-