Commit 561d4ef
fix: render a null struct row as an empty CSV cell, not {"field":null,...}
A nested Struct-dtype column whose entire row is null rendered as a JSON
object with every field null (e.g. {"id":null,"label":null}) instead of an
empty CSV cell, diverging from the parquet oracle and from every other
nullable column type in CsvExporter (discovered on the Raincloud oasst1
`labels` column while validating #268).
Root cause: struct-row nullability is represented in this codebase by
masking EACH field invalid (StructEncodingDecoder / StructLayoutDecoder wrap
every field array in the shared struct validity), never by wrapping the whole
StructArray in one MaskedArray. So a null struct row decodes as a StructArray
whose fields are each MaskedArray-invalid; the StructArray itself is never
masked. CsvExporter.cellValue routed a StructArray straight to jsonObject,
which called jsonValue per field — each field's own MaskedArray arm rendered
"null" — producing {"field":null,...} instead of an empty cell.
Fix at the exporter, not the decoder. The per-field masking is deliberate and
load-bearing: ScanIterator.expandStruct relies on the root StructArray being
un-wrapped (its `instanceof StructArray` checks) and on each field carrying
the struct-row validity so an expanded root column is null when the struct row
is null. Wrapping the whole StructArray once would break that core scan path
for every root struct and remove per-column validity propagation — a wide,
risky change. The actual invariant is intact; the bug is purely that the
exporter never recognized "every field invalid" as "the row is null". A new
isNullStructRow(StructArray, long) helper detects that, and cellValue's and
jsonValue's StructArray arms now render an empty cell / JSON null respectively.
Added CsvExporterTest#rendersNullStructRowAsEmptyCell: a nested struct column
with an all-fields-null row (the exact issue shape) asserts the cell is empty,
not {"id":null,"label":null}. Verified it fails on the pre-fix exporter with
the reported bad output.
Review round (#275): added
CsvExporterTest#rendersNullStructElementInListAsJsonNull to cover the nested
jsonValue StructArray arm — a null struct element inside a List[Struct] must
render as JSON null, not {"ref":null,...} — which was previously untested.
Also clarified isNullStructRow's javadoc: it detects "every field invalid at
this row", which is exact for struct-level-validity sources and an accepted
approximation (indistinguishable from "present with all-null fields") for
per-field-validity sources, matching the parquet oracle for the oasst1 case
and unambiguous in practice since the writer cannot emit struct-level validity.
Fixes #270
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>1 parent 218c72f commit 561d4ef
2 files changed
Lines changed: 129 additions & 9 deletions
File tree
- csv/src
- main/java/io/github/dfa1/vortex/csv
- test/java/io/github/dfa1/vortex/csv
Lines changed: 38 additions & 9 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
148 | 148 | | |
149 | 149 | | |
150 | 150 | | |
151 | | - | |
152 | | - | |
153 | | - | |
154 | | - | |
155 | | - | |
156 | | - | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
157 | 158 | | |
158 | 159 | | |
159 | 160 | | |
| |||
192 | 193 | | |
193 | 194 | | |
194 | 195 | | |
195 | | - | |
196 | | - | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
197 | 200 | | |
198 | 201 | | |
199 | 202 | | |
| |||
218 | 221 | | |
219 | 222 | | |
220 | 223 | | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
221 | 250 | | |
222 | 251 | | |
223 | 252 | | |
224 | 253 | | |
225 | 254 | | |
226 | 255 | | |
227 | 256 | | |
228 | | - | |
| 257 | + | |
229 | 258 | | |
230 | 259 | | |
231 | 260 | | |
| |||
Lines changed: 91 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
| 11 | + | |
11 | 12 | | |
12 | 13 | | |
13 | 14 | | |
| |||
348 | 349 | | |
349 | 350 | | |
350 | 351 | | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
351 | 395 | | |
352 | 396 | | |
353 | 397 | | |
| |||
521 | 565 | | |
522 | 566 | | |
523 | 567 | | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
| 614 | + | |
524 | 615 | | |
0 commit comments