diff --git a/vortex-layout/src/layouts/struct_/reader.rs b/vortex-layout/src/layouts/struct_/reader.rs index 30547d298eb..e0d2594ec41 100644 --- a/vortex-layout/src/layouts/struct_/reader.rs +++ b/vortex-layout/src/layouts/struct_/reader.rs @@ -320,6 +320,17 @@ fn expand_struct_root( .first() .is_some_and(BoundExpression::is_root) { + // Masking `$.x` inside `$.x.q` would hide the path from the partitioner, which + // would then read all of `x`. Mask the whole path instead. + if validity.is_some() + && let Some(path) = field_path(&node, packed, fields)? + { + return Ok(Transformed { + value: with_validity(path), + changed: true, + order: TraversalOrder::Skip, + }); + } return Ok(Transformed::no(node)); } @@ -380,6 +391,35 @@ fn expand_struct_root( .into_inner()) } +/// Rebuilds a chain of `get_item` and `select` that starts at `$.field` on the field's expression +/// in `packed`, or returns `None` if `node` is not such a chain. +fn field_path( + node: &BoundExpression, + packed: &BoundExpression, + fields: &StructFields, +) -> VortexResult> { + let Some(scalar_fn) = node.as_scalar() else { + return Ok(None); + }; + if !scalar_fn.is::() && !scalar_fn.is::