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:

Adds importTransactions, which streams a statement CSV into the ledger table 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.

  1. 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 …,1 whose next chunk starts 250.00 is 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 use readline or a proper CSV parser that streams records.
  2. Async listener: no backpressure and an early resolve (line 22, critical). The stream does not wait for an async data listener. 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, and end fires and resolves summary long before the inserts finish. The worker then reconciles against partial totals. Consume the stream with for await (const line of rl), or pause and resume, and resolve only after the last insert.
  3. Insert failures become unhandled rejections (lines 41–44, major). A rejected pool.query rejects 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.
  4. Naive comma split (lines 33–34, major). "Refund, order 2" shifts the columns, so amount becomes ' order 2"', and the lenient parseFloat turns ' 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}$/.
  5. Rounding down floating-point pence (line 39, major). 19.99 * 100 is 1998.9999999999998, so Math.floor stores 1998 and a penny is lost on many rows. Parse the pounds and pence as integers from the string, or at least use Math.round.
  6. Multi-byte characters cut at chunk edges (line 23, minor). chunk.toString on a chunk that ends halfway through é or £ produces replacement characters in descriptions. Call stream.setEncoding("utf8") or use a StringDecoder, which carries partial characters over.
Next Post Previous Post
No Comment
Add Comment
comment url