Fixed reading nullable nested (struct) columns from Parquet/Arrow - #2043
Conversation
… update documentation This commit introduces tests for handling nullable and nested Arrow `Struct` types to ensure accurate representation in Kotlin DataFrame. Additionally, it updates the Parquet documentation to explain how nullable Arrow `Struct`s are mapped to `ColumnGroup`s, addressing limitations and behavior when leaf fields are null or absent.
There was a problem hiding this comment.
Pull request overview
Fixes nullable Arrow/Parquet struct reads by propagating parent nulls into nested child columns.
Changes:
- Adds recursive null propagation for structs and list elements.
- Adds comprehensive IPC, Feather, and Parquet tests and fixtures.
- Documents nullable struct representation limitations.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
arrowReadingImpl.kt |
Implements struct null propagation. |
arrowReading.kt |
Documents Parquet struct behavior. |
ArrowNullableStructTest.kt |
Adds nullable-struct tests. |
arrowTestUtils.kt |
Adds Arrow serialization helpers. |
ArrowKtTest.kt |
Reuses shared test helpers. |
TestFiles.md |
Documents test fixtures. |
nullable_nested_struct.parquet |
Adds regression fixture. |
Parquet.md |
Updates nested struct documentation. |
Suppressed comments (5)
dataframe-arrow/src/test/kotlin/org/jetbrains/kotlinx/dataframe/io/ArrowKtTest.kt:57
Ignoreis unused after the fixture-regeneration test was placed inArrowNullableStructTest; this fails the ktlint unused-import check.
import org.junit.Ignore
dataframe-arrow/src/test/kotlin/org/jetbrains/kotlinx/dataframe/io/ArrowKtTest.kt:67
- The refactor removes the last use of
Channels, whileFilesandStandardCopyOptionare newly added but unused. Remove all three imports to satisfy the ktlint unused-import check.
import java.nio.channels.Channels
import java.nio.file.Files
import java.nio.file.StandardCopyOption
dataframe-arrow/src/test/kotlin/org/jetbrains/kotlinx/dataframe/io/ArrowKtTest.kt:24
- The new helper removes this file's last uses of
VectorSchemaRoot,ArrowFileWriter, andArrowStreamWriter; the newly added vector imports are also unused. Remove all six stale imports so the ktlint unused-import check can pass.
import org.apache.arrow.vector.VarCharVector
import org.apache.arrow.vector.VectorSchemaRoot
import org.apache.arrow.vector.complex.ListVector
import org.apache.arrow.vector.complex.StructVector
dataframe-arrow/src/test/kotlin/org/jetbrains/kotlinx/dataframe/io/ArrowNullableStructTest.kt:29
ByteArrayOutputStreamis unused because the shared Arrow test utility owns the output stream. This import will fail the ktlint unused-import check.
import java.io.ByteArrayOutputStream
dataframe-arrow/src/test/kotlin/org/jetbrains/kotlinx/dataframe/io/ArrowNullableStructTest.kt:32
Channelsis unused after stream serialization was moved intoarrowTestUtils.kt; remove it to satisfy ktlint.
import java.nio.channels.Channels
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| ColumnKind.Frame -> | ||
| DataColumn.createFrameColumn( | ||
| name = name(), | ||
| groups = (this as FrameColumn<*>).toList().mapIndexed { i, frame -> | ||
| if (isNull[i]) DataFrame.empty() else frame | ||
| }, | ||
| ) |
Revised how nested nullable Arrow `Struct`s are read as `ColumnGroup`s, aligning with Parquet behavior. Replaced outdated Arrow documentation links with current ones across all modules. Simplified null handling logic in Arrow struct reading and added clarifications to related test cases.
Fixes #2041
Fix reading nullable nested (struct) columns from Parquet/Arrow
Problem it fixes
When reading a Parquet or Arrow file, a nullable nested column (an "optional group",
e.g.
record { x, y }) was read incorrectly. In rows where the group is absent, theDataFrame showed fake data —
{x:0, y:0}, or values leaked from another row — and thenested columns were typed as non-null.
Cause: Arrow stores a separate "is-null" bit for the struct itself, independent of its
child arrays. The reader ignored this bit and read the child values directly. Under a
null row those child values are undefined, so we got garbage.
What this PR does
row, all of its child cells become
nulland the child column types become nullable.element inside a list is handled the same way.
IPC and Feather.
How it works (short)
A
ColumnGroupcannot benullper row, so the null is pushed down into the childcolumns. A small recursive helper
injectNullsAtsets the child cells tonull:null(type widened to nullable);null).It dispatches on the column kind (public
ColumnGroup/FrameColumninterfaces, noimpl details) and returns a column unchanged when there is nothing to null, so no data
is copied when there are no nulls.
Files
arrowReadingImpl.kt— the fix: consult the struct validity buffer,injectNullsAthelper, and the same handling for null struct elements in
readListVector; removed apiece of dead code.
arrowReading.kt— KDoc note onreadParquetabout the behavior.docs/.../Parquet.md— updated (the old "Struct not supported" note was stale).ArrowNullableStructTest.kt(all the nullable-struct cases), a sharedarrowTestUtils.kthelper, the committed fixturenullable_nested_struct.parquet+TestFiles.mddescribing it.Tests
a fix relying on zeros would be caught; one runs in
NullabilityOptions.Checking.children and nulls at two levels at once.
fixture. The fixture is produced by our own code (Arrow
DatasetFileWriter) and canbe regenerated on any OS; DuckDB / the DataFrame writer can't produce this layout, so
they are not used here.
Known limitations (not fixed here, on purpose)
ColumnGroupcannot benullper row, so an absent group and a present group withall-null children look the same (
{x:null, y:null}). The exact type{x:Int, y:Int}?from the issue would need a larger change in core.
every struct row as present).