Use spanvalue WriteRowIterator for experimental CSV#39
Conversation
Replace the hand-rolled Next/PrepareRowType/Flush loop with WriteRowIterator, including the redact-rows path after skipRowIter. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Code Review
This pull request simplifies writeCsvFromRowIter by replacing manual row iteration with svwriter.WriteRowIterator. However, removing defer rowIter.Stop() introduces a potential resource leak if skipRowIter returns an error early when redactRows is true.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
apstndb
left a comment
There was a problem hiding this comment.
Nice cleanup. I traced both paths against the spanner v1.84.1 and spanvalue v0.5.0 sources and the refactor is behavior-preserving:
- non-redact:
WriteRowIterator(runRowIterator,writer/row_iterator.go:252-285) runs the exact same logic as the old hand-rolled loop —PrepareMetadataon the firstNext(even when it'siterator.Done),WriteRowper row,FlushinFinish. Same header/row output. - redact:
skipRowIter->RowIterator.Dodrains toDoneand always callsStop(spanner/read.go:269), leavingr.err == iterator.Done. The followingWriteRowIterator's firstNextshort-circuits on thatr.err(read.go:162) and returnsDone, so it emits header-only and Flushes — same output as the oldprepareCsvRowType+Flush. - No Stop leak:
Doalways Stops (covers theskipRowItererror path too) andWriteRowIteratoralways Stops, so droppingdefer rowIter.Stop()is safe. The only theoretical gap is the unreachableNewCSVWritererror before the iterator is ever driven — negligible. go build ./...andgo test -run TestExperimentalCsv .pass.
Two non-blocking points inline, plus one coverage note:
Test coverage: writeCsvFromRowIter (the production CSV streaming path, both normal and redact) is not exercised by any unit or integration test — csv_test.go covers writeCsvFromResultSet and a separate hand-rolled writeCsvFromPreparedRows, and integration_test.go has no experimental_csv case. This refactor touches the one CSV path that has no regression test. Worth an emulator-backed (spanemuboost) test that runs writeCsvFromRowIter for normal, zero-row, and --redact-rows, especially since the redact header-only behavior is the subtle part.
WriteRowIterator drains the iterator once; csvRedactRowIteratorWriter skips WriteRow bodies while still emitting the header. Drop skipRowIter before WriteRowIterator and pass the query iterator directly from the single path. Co-authored-by: Cursor <cursoragent@cursor.com>
apstndb
left a comment
There was a problem hiding this comment.
Re-reviewed at eecae90. The new csvRedactRowIteratorWriter approach cleanly resolves my earlier concern about re-driving a drained+stopped iterator — both paths now go through a single WriteRowIterator call that owns Stop, with no double-Stop and no reliance on Next-after-Done semantics. Nice.
Verified the new design against the spanvalue v0.5.0 source:
RowIteratorWriterembedsFlushWriter(=Writer+Flusher), so it includesWriteRow,Flush, andPrepareRowType.RowIteratorHooksFromWritercapturesw.WriteRowfrom the interface value, so dynamic dispatch pickscsvRedactRowIteratorWriter's explicit nopWriteRow(which shadows the promoted*DelimitedWriter.WriteRow). Redact output is therefore header-only:PrepareMetadata-> header, nopWriteRowdiscards each row,Finish->Flush.- The embedded field is a pointer (
*svwriter.DelimitedWriter), so thecsvRedactRowIteratorWritervalue satisfiesRowIteratorWriter(promoted pointer methods are in the value's method set). skipRowIteris no longer used by the CSV path but is still used atmain.go:577(JSON/jq redact) and inintegration_test.go, so it is not dead code.go build ./...,go vet ./..., andgo test -run TestExperimentalCsv .all pass.
Note: redact still streams every row from the server and discards it (same as the old skipRowIter/Do), so there is no behavior change there — redact suppresses output, not the scan.
One remaining ask inline: this redact path is still untested, and its correctness now hinges on Go method-shadowing through an interface.
Cover writeCsvFromRowIter for normal rows, zero-row header-only SELECT, and redact-rows header-only output. Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
RowIteratorloop inwriteCsvFromRowIterwithsvwriter.WriteRowIterator.--redact-rowsasskipRowIterfollowed byWriteRowIterator(header-only when metadata has columns).WriteRowIteratorownsStop,PrepareRowTypeon firstNext, andFlushvia hooks.Why
Aligns experimental CSV with spanvalue v0.5.0 documented RowIterator path (zero-row
SELECT, redact) and drops ~25 lines of duplicate lifecycle code.Test plan
go test ./...Made with Cursor