Review: Streaming CSV Import Written by an AI Assistant
Your team asked an AI coding assistant to import bank statement CSV files into a small-business accounting product. It opened this pull request. You are the reviewer, and nothing merges without your approval.
The assistant's description:
AddsimportTransactions, which streams a statement CSV into theledgertable row by row, so even very large statements use almost no memory. Returns a summary with rows imported, rows skipped and the total in pence, which the caller uses to reconcile against the statement's closing balance.
Where it runs: a Node 20 worker that imports uploaded statements from a queue. Statements range from a few rows to 400,000 rows (about 30 MB). Banks quote fields that contain commas, such as "Tesco, Express", and descriptions often contain accented letters and the £ sign. The ledger.id column is unique, and banks sometimes repeat an id. As soon as the promise resolves, the worker checks totalPence against the statement and marks the import complete or failed.
Your task
- Flag every problem you would block the merge for. Click a line number to comment, and Shift-click to cover a range.
- For each one, choose the issue type and severity, and explain what fails and how to fix it.
- You are scored on the problems you catch, the right issue type and a clear explanation. Missing a critical problem fails the round.
Aim for 20 minutes, as in a real review round.
A read stream hands you 64 KB chunks. Does a chunk always end at the end of a line?
The data listener is async. Does the stream wait for it before emitting the next chunk, or before end?
One row has a duplicate id and the insert fails. Where does that error go?
Split 7,2026-03-01,"Refund, order 2",0.50 on commas. Which value ends up in amount, and what does parseFloat make of it?
Work out Math.floor(19.99 * 100). Then think about a two-byte character such as é landing at the very end of a chunk.
A strong reviewer blocks this because it silently imports wrong amounts and reports a summary before the work is done, which defeats the reconciliation that is meant to catch mistakes.
- Lines split across chunk boundaries (line 24, critical). Each 64 KB chunk is split on its own, so the last line of a chunk is cut in two. A row ending
…,1whose next chunk starts250.00is imported as £1 instead of £1,250, and the tail becomes a broken row that is skipped. Keep the unfinished last line and prepend it to the next chunk, or usereadlineor a proper CSV parser that streams records. - Async listener: no backpressure and an early resolve (line 22, critical). The stream does not wait for an
asyncdatalistener. Chunks keep arriving while inserts are pending, so for a large file thousands of queries pile up in the pool queue in memory, rows from different chunks interleave, andendfires and resolvessummarylong before the inserts finish. The worker then reconciles against partial totals. Consume the stream withfor await (const line of rl), or pause and resume, and resolve only after the last insert. - Insert failures become unhandled rejections (lines 41–44, major). A rejected
pool.queryrejects the listener's promise, which no one handles: Node 20 exits, the returned promise never settles, and the rows inserted so far stay in the ledger. Run the import in one transaction, roll back on error and reject the promise, or upsert so a retry is safe. - Naive comma split (lines 33–34, major).
"Refund, order 2"shifts the columns, soamountbecomes' order 2"', and the lenientparseFloatturns' 3"'from a description like"Table, 3"into 3. Rows are skipped or imported with invented amounts. Use a real CSV parser and validate the amount with a strict pattern such as/^-?\d+\.\d{2}$/. - Rounding down floating-point pence (line 39, major).
19.99 * 100is1998.9999999999998, soMath.floorstores 1998 and a penny is lost on many rows. Parse the pounds and pence as integers from the string, or at least useMath.round. - Multi-byte characters cut at chunk edges (line 23, minor).
chunk.toStringon a chunk that ends halfway through é or £ produces replacement characters in descriptions. Callstream.setEncoding("utf8")or use aStringDecoder, which carries partial characters over.