Skip to content

Add a constructor for an all-null column - #42

Merged
emilk merged 2 commits into
release-0.6from
emilk/column-new-null
Aug 27, 2026
Merged

emilk merged 2 commits into
release-0.6from
emilk/column-new-null

Conversation

@emilk

@emilk emilk commented Aug 27, 2026

Copy link
Copy Markdown
Member

Related

What

Column::<Option<L>>::new_null(len) — a column of nulls, built with arrow::array::new_null_array instead of going through the values path and naming a type the column never holds. Same on TypedArray.

ColumnDesc::<Option<L>>::new_null(len) does it from a descriptor, carrying the declared metadata over, so DESC.optional().new_null(n) pads a record batch without repeating the column name.

ColumnDesc::arrow_metadata() single-sources the declared metadata that arrow_field and new_null both need.

Run-end encoding is the one exception: a RunArray has no validity of its own, so Option<Run<K, V>> is unbuildable by any route (from_nullable_values already errors on it). new_null panics there, documented and regression-tested.

Testing

cargo test --all-features. New new_null test covers zero length, nesting, dictionary, fixed-size binary, the TypedArray form, and the descriptor round-trip through a record batch.

Compatibility

Additive.

`Column::<Option<L>>::new_null(len)` (and the `TypedArray` form) build a
column of nulls directly, with `arrow::array::new_null_array`. Before, that
went through the values path and had to name a type the column never holds:

    Column::<Option<Binary>>::from_nullable_values(
        std::iter::repeat_n(None::<Vec<u8>>, num_rows),
    )

`ColumnDesc::<Option<L>>::new_null(len)` does the same from a descriptor,
carrying the declared metadata over, so `DESC.optional().new_null(n)` pads a
record batch that is missing the column without repeating its name.

Run-end encoding is the one exception: a `RunArray` has no validity of its
own, so `Option<Run<K, V>>` is unbuildable by any route. Documented, with a
regression test.

Closes #39

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread crates/quiver_types/src/column_desc.rs Outdated
Single-sources the declared metadata that `arrow_field` and `new_null` both
need, and lets a caller stamp it on a field they build themselves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@emilk
emilk marked this pull request as ready for review August 27, 2026 18:40
@emilk
emilk merged commit c814181 into release-0.6 Aug 27, 2026
6 checks passed

@emilk emilk left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Post-merge review, as part of reviewing the 0.6 stack for #30. Clippy and tests are clean at the tip of release-0.6. new_null earns its place — from_nullable_values(repeat_n(None::<Vec<u8>>, len)) really does make you name a type the column never holds — and routing it through the descriptor so DESC.optional().new_null(n) doesn't restate the name is what makes it fit the rest of the API.

The run-end panic is length-dependent. Verified at the tip of release-0.6:

Column::<Option<Run<i32, Utf8>>>::new_null(0)   // succeeds, len 0
Column::<Option<Run<i32, Utf8>>>::new_null(2)   // panics

new_null_array for a zero-length RunEndEncoded produces empty run-ends and empty values, so there are no child nulls for Run's downcast to reject and the expect never fires. So the documented "Panics for run-end encoding" holds for every length except the one people pass in edge-case tests. Either check the datatype up front, or narrow the doc to a non-empty run-end column and add the len = 0 case to the test. Related: #37's optional() also reaches Column<Option<Run<K, V>>>, so "which no constructor can build" isn't right either — both notes probably resolve into one paragraph in Run's docs.

The panic is doing the type system's job. This crate otherwise excludes impossible cases at compile time: PrimitiveType gates as_slice, RefType gates Index, InfallibleBuild gates from_values. new_null compiles for every ConcreteType and panics for one. InfallibleBuild is the wrong bound (your test proves Option<Dictionary<i32, Utf8>>::new_null works), but a marker for "this encoding has a top-level validity buffer" moves it to the type system and deletes the expect — the only expect in the crate a user can trigger from safe, obvious code.

Minor: ColumnDesc::new_null goes arrow_metadata()HashMap → re-collect into BTreeMap; iterating self.metadata directly skips the intermediate. And the two impl<L: ConcreteType> Column<Option<L>> blocks are adjacent with the same bound — typed_array.rs correctly puts new_null in the existing block, so the two files disagree.

— Claude

@emilk emilk mentioned this pull request Aug 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant