diff --git a/vortex-layout/src/layouts/list/reader.rs b/vortex-layout/src/layouts/list/reader.rs index 642e6f3f07a..6f27c1abd4b 100644 --- a/vortex-layout/src/layouts/list/reader.rs +++ b/vortex-layout/src/layouts/list/reader.rs @@ -2202,6 +2202,7 @@ mod tests { #[case::two_cuts(&[1, 3], false, false)] #[case::null_and_empty_at_cuts(&[1, 3], true, false)] #[case::list_view_chunks(&[1, 3], true, true)] + #[case::non_nullable_list_view_chunks(&[1, 3], false, true)] #[tokio::test] async fn sliced_list_chunks_round_trip( #[case] cuts: &[usize], diff --git a/vortex-layout/src/layouts/list/writer.rs b/vortex-layout/src/layouts/list/writer.rs index e447f2037ab..1e4d9a459cc 100644 --- a/vortex-layout/src/layouts/list/writer.rs +++ b/vortex-layout/src/layouts/list/writer.rs @@ -295,15 +295,16 @@ fn canonicalize_to_list_parts( exec_ctx: &mut ExecutionCtx, ) -> VortexResult { let canonical = array.execute_until::(exec_ctx)?; - if let Some(list) = canonical.as_opt::() { - // A sliced list keeps its parent's whole `elements`, but `global_offsets` assumes each - // chunk's offsets span exactly `[0, elements.len()]`. - Ok(list.reset_offsets(false, exec_ctx)?.into_data_parts()) + let list = if let Some(list) = canonical.as_opt::() { + list.into_owned() } else if let Some(view) = canonical.as_opt::() { - Ok(list_from_list_view(view.into_owned(), exec_ctx)?.into_data_parts()) + list_from_list_view(view.into_owned(), exec_ctx)? } else { unreachable!("AnyList matcher guarantees List or ListView") - } + }; + // Sliced lists can retain unused elements, and rebuilding a list view can leave a lazy + // elements slice. Normalize both before passing their children to serializing strategies. + Ok(list.reset_offsets(false, exec_ctx)?.into_data_parts()) } /// Rebase a chunk's local `offsets` into global `u64` positions for the whole-column `offsets`