• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

vortex-data / vortex / 16049945385

03 Jul 2025 12:06PM UTC coverage: 77.865% (-0.02%) from 77.885%
16049945385

push

github

web-flow
feat: Hide as many of FieldNames' Arcs as possible (#3747)

The main purpose of this PR is to hide the implementation details of
`FieldNames` from the public API, and pushing all the required
`from/into` calls internally.

The change in API can be seen in all the test changes, which I think are
nicer.

---------

Signed-off-by: Adam Gutglick <adam@spiraldb.com>

138 of 173 new or added lines in 30 files covered. (79.77%)

43324 of 55640 relevant lines covered (77.86%)

55702.55 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.79
/vortex-expr/src/exprs/pack.rs
1
use std::any::Any;
2
use std::fmt::Display;
3
use std::hash::Hash;
4
use std::sync::Arc;
5

6
use itertools::Itertools as _;
7
use vortex_array::arrays::StructArray;
8
use vortex_array::validity::Validity;
9
use vortex_array::{ArrayRef, IntoArray};
10
use vortex_dtype::{DType, FieldName, FieldNames, Nullability, StructFields};
11
use vortex_error::{VortexExpect as _, VortexResult, vortex_bail, vortex_err};
12

13
use crate::{AnalysisExpr, ExprRef, Scope, ScopeDType, VortexExpr};
14

15
/// Pack zero or more expressions into a structure with named fields.
16
///
17
/// # Examples
18
///
19
/// ```
20
/// use vortex_array::{IntoArray, ToCanonical};
21
/// use vortex_buffer::buffer;
22
/// use vortex_expr::{root, Pack, Scope, VortexExpr};
23
/// use vortex_scalar::Scalar;
24
/// use vortex_dtype::Nullability;
25
///
26
/// let example = Pack::try_new_expr(
27
///     ["x", "x copy", "second x copy"].into(),
28
///     vec![root(), root(), root()],
29
///     Nullability::NonNullable,
30
/// ).unwrap();
31
/// let packed = example.evaluate(&Scope::new(buffer![100, 110, 200].into_array())).unwrap();
32
/// let x_copy = packed
33
///     .to_struct()
34
///     .unwrap()
35
///     .field_by_name("x copy")
36
///     .unwrap()
37
///     .clone();
38
/// assert_eq!(x_copy.scalar_at(0).unwrap(), Scalar::from(100));
39
/// assert_eq!(x_copy.scalar_at(1).unwrap(), Scalar::from(110));
40
/// assert_eq!(x_copy.scalar_at(2).unwrap(), Scalar::from(200));
41
/// ```
42
///
43
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
44
pub struct Pack {
45
    names: FieldNames,
46
    values: Vec<ExprRef>,
47
    nullability: Nullability,
48
}
49

50
impl Pack {
51
    pub fn try_new_expr(
2,989✔
52
        names: FieldNames,
2,989✔
53
        values: Vec<ExprRef>,
2,989✔
54
        nullability: Nullability,
2,989✔
55
    ) -> VortexResult<ExprRef> {
2,989✔
56
        if names.len() != values.len() {
2,989✔
57
            vortex_bail!("length mismatch {} {}", names.len(), values.len());
×
58
        }
2,989✔
59
        Ok(Arc::new(Pack {
2,989✔
60
            names,
2,989✔
61
            values,
2,989✔
62
            nullability,
2,989✔
63
        }))
2,989✔
64
    }
2,989✔
65

66
    pub fn names(&self) -> &FieldNames {
×
67
        &self.names
×
68
    }
×
69

70
    pub fn field(&self, field_name: &FieldName) -> VortexResult<ExprRef> {
979✔
71
        let idx = self
979✔
72
            .names
979✔
73
            .iter()
979✔
74
            .position(|name| name == field_name)
980✔
75
            .ok_or_else(|| {
979✔
76
                vortex_err!(
×
77
                    "Cannot find field {} in pack fields {:?}",
×
78
                    field_name,
×
79
                    self.names
×
80
                )
×
81
            })?;
979✔
82

83
        self.values
979✔
84
            .get(idx)
979✔
85
            .cloned()
979✔
86
            .ok_or_else(|| vortex_err!("field index out of bounds: {}", idx))
979✔
87
    }
979✔
88

89
    pub fn nullability(&self) -> Nullability {
×
90
        self.nullability
×
91
    }
×
92
}
93

94
pub fn pack(
1,820✔
95
    elements: impl IntoIterator<Item = (impl Into<FieldName>, ExprRef)>,
1,820✔
96
    nullability: Nullability,
1,820✔
97
) -> ExprRef {
1,820✔
98
    let (names, values): (Vec<_>, Vec<_>) = elements
1,820✔
99
        .into_iter()
1,820✔
100
        .map(|(name, value)| (name.into(), value))
2,075✔
101
        .unzip();
1,820✔
102
    Pack::try_new_expr(names.into(), values, nullability)
1,820✔
103
        .vortex_expect("pack names and values have the same length")
1,820✔
104
}
1,820✔
105

106
impl Display for Pack {
107
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
108
        write!(
×
109
            f,
×
110
            "pack({{{}}}){}",
×
111
            self.names
×
112
                .iter()
×
113
                .zip(&self.values)
×
114
                .format_with(", ", |(name, expr), f| f(&format_args!("{name}: {expr}"))),
×
115
            self.nullability
×
116
        )
×
117
    }
×
118
}
119

120
#[cfg(feature = "proto")]
121
pub(crate) mod proto {
122
    use vortex_error::{VortexResult, vortex_bail};
123
    use vortex_proto::expr::kind;
124
    use vortex_proto::expr::kind::Kind;
125

126
    use crate::{ExprDeserialize, ExprRef, ExprSerializable, Id, Pack};
127

128
    pub struct PackSerde;
129

130
    impl Id for PackSerde {
131
        fn id(&self) -> &'static str {
2✔
132
            "pack"
2✔
133
        }
2✔
134
    }
135

136
    impl ExprDeserialize for PackSerde {
137
        fn deserialize(&self, kind: &Kind, children: Vec<ExprRef>) -> VortexResult<ExprRef> {
×
138
            let Kind::Pack(op) = kind else {
×
139
                vortex_bail!("wrong kind {:?}, wanted pack", kind)
×
140
            };
141

142
            Pack::try_new_expr(
×
NEW
143
                op.paths.iter().map(|p| p.as_str()).collect(),
×
144
                children,
×
145
                op.nullable.into(),
×
146
            )
×
147
        }
×
148
    }
149

150
    impl ExprSerializable for Pack {
151
        fn id(&self) -> &'static str {
×
152
            PackSerde.id()
×
153
        }
×
154

155
        fn serialize_kind(&self) -> VortexResult<Kind> {
×
156
            Ok(Kind::Pack(kind::Pack {
×
157
                paths: self.names.iter().map(|n| n.to_string()).collect(),
×
158
                nullable: self.nullability.into(),
×
159
            }))
×
160
        }
×
161
    }
162
}
163

164
impl AnalysisExpr for Pack {}
165

166
impl VortexExpr for Pack {
167
    fn as_any(&self) -> &dyn Any {
19,660✔
168
        self
19,660✔
169
    }
19,660✔
170

171
    fn unchecked_evaluate(&self, scope: &Scope) -> VortexResult<ArrayRef> {
908✔
172
        let len = scope.len();
908✔
173
        let value_arrays = self
908✔
174
            .values
908✔
175
            .iter()
908✔
176
            .map(|value_expr| value_expr.unchecked_evaluate(scope))
1,208✔
177
            .process_results(|it| it.collect::<Vec<_>>())?;
908✔
178
        let validity = match self.nullability {
908✔
179
            Nullability::NonNullable => Validity::NonNullable,
907✔
180
            Nullability::Nullable => Validity::AllValid,
1✔
181
        };
182
        Ok(StructArray::try_new(self.names.clone(), value_arrays, len, validity)?.into_array())
908✔
183
    }
908✔
184

185
    fn children(&self) -> Vec<&ExprRef> {
18,916✔
186
        self.values.iter().collect()
18,916✔
187
    }
18,916✔
188

189
    fn replacing_children(self: Arc<Self>, children: Vec<ExprRef>) -> ExprRef {
1,084✔
190
        assert_eq!(children.len(), self.values.len());
1,084✔
191
        Self::try_new_expr(self.names.clone(), children, self.nullability)
1,084✔
192
            .vortex_expect("children are known to have the same length as names")
1,084✔
193
    }
1,084✔
194

195
    fn return_dtype(&self, scope: &ScopeDType) -> VortexResult<DType> {
1,313✔
196
        let value_dtypes = self
1,313✔
197
            .values
1,313✔
198
            .iter()
1,313✔
199
            .map(|value_expr| value_expr.return_dtype(scope))
1,661✔
200
            .process_results(|it| it.collect())?;
1,313✔
201
        Ok(DType::Struct(
1,313✔
202
            StructFields::new(self.names.clone(), value_dtypes),
1,313✔
203
            self.nullability,
1,313✔
204
        ))
1,313✔
205
    }
1,313✔
206
}
207

208
#[cfg(test)]
209
mod tests {
210

211
    use vortex_array::arrays::{PrimitiveArray, StructArray};
212
    use vortex_array::validity::Validity;
213
    use vortex_array::vtable::ValidityHelper;
214
    use vortex_array::{Array, ArrayRef, IntoArray, ToCanonical};
215
    use vortex_buffer::buffer;
216
    use vortex_dtype::{FieldNames, Nullability};
217
    use vortex_error::{VortexResult, vortex_bail};
218

219
    use crate::{Pack, Scope, col};
220

221
    fn test_array() -> ArrayRef {
4✔
222
        StructArray::from_fields(&[
4✔
223
            ("a", buffer![0, 1, 2].into_array()),
4✔
224
            ("b", buffer![4, 5, 6].into_array()),
4✔
225
        ])
4✔
226
        .unwrap()
4✔
227
        .into_array()
4✔
228
    }
4✔
229

230
    fn primitive_field(array: &dyn Array, field_path: &[&str]) -> VortexResult<PrimitiveArray> {
7✔
231
        let mut field_path = field_path.iter();
7✔
232

233
        let Some(field) = field_path.next() else {
7✔
234
            vortex_bail!("empty field path");
×
235
        };
236

237
        let mut array = array.to_struct()?.field_by_name(field)?.clone();
7✔
238
        for field in field_path {
9✔
239
            array = array.to_struct()?.field_by_name(field)?.clone();
2✔
240
        }
241
        Ok(array.to_primitive().unwrap())
7✔
242
    }
7✔
243

244
    #[test]
245
    pub fn test_empty_pack() {
1✔
246
        let expr = Pack::try_new_expr(FieldNames::default(), Vec::new(), Nullability::NonNullable)
1✔
247
            .unwrap();
1✔
248

1✔
249
        let test_array = test_array();
1✔
250
        let actual_array = expr.evaluate(&Scope::new(test_array.clone())).unwrap();
1✔
251
        assert_eq!(actual_array.len(), test_array.len());
1✔
252
        assert_eq!(
1✔
253
            actual_array.to_struct().unwrap().struct_fields().nfields(),
1✔
254
            0
1✔
255
        );
1✔
256
    }
1✔
257

258
    #[test]
259
    pub fn test_simple_pack() {
1✔
260
        let expr = Pack::try_new_expr(
1✔
261
            ["one", "two", "three"].into(),
1✔
262
            vec![col("a"), col("b"), col("a")],
1✔
263
            Nullability::NonNullable,
1✔
264
        )
1✔
265
        .unwrap();
1✔
266

1✔
267
        let actual_array = expr
1✔
268
            .evaluate(&Scope::new(test_array()))
1✔
269
            .unwrap()
1✔
270
            .to_struct()
1✔
271
            .unwrap();
1✔
272
        let expected_names: FieldNames = ["one", "two", "three"].into();
1✔
273
        assert_eq!(actual_array.names(), &expected_names);
1✔
274
        assert_eq!(actual_array.validity(), &Validity::NonNullable);
1✔
275

276
        assert_eq!(
1✔
277
            primitive_field(actual_array.as_ref(), &["one"])
1✔
278
                .unwrap()
1✔
279
                .as_slice::<i32>(),
1✔
280
            [0, 1, 2]
1✔
281
        );
1✔
282
        assert_eq!(
1✔
283
            primitive_field(actual_array.as_ref(), &["two"])
1✔
284
                .unwrap()
1✔
285
                .as_slice::<i32>(),
1✔
286
            [4, 5, 6]
1✔
287
        );
1✔
288
        assert_eq!(
1✔
289
            primitive_field(actual_array.as_ref(), &["three"])
1✔
290
                .unwrap()
1✔
291
                .as_slice::<i32>(),
1✔
292
            [0, 1, 2]
1✔
293
        );
1✔
294
    }
1✔
295

296
    #[test]
297
    pub fn test_nested_pack() {
1✔
298
        let expr = Pack::try_new_expr(
1✔
299
            ["one", "two", "three"].into(),
1✔
300
            vec![
1✔
301
                col("a"),
1✔
302
                Pack::try_new_expr(
1✔
303
                    ["two_one", "two_two"].into(),
1✔
304
                    vec![col("b"), col("b")],
1✔
305
                    Nullability::NonNullable,
1✔
306
                )
1✔
307
                .unwrap(),
1✔
308
                col("a"),
1✔
309
            ],
1✔
310
            Nullability::NonNullable,
1✔
311
        )
1✔
312
        .unwrap();
1✔
313

1✔
314
        let actual_array = expr
1✔
315
            .evaluate(&Scope::new(test_array()))
1✔
316
            .unwrap()
1✔
317
            .to_struct()
1✔
318
            .unwrap();
1✔
319
        let expected_names = FieldNames::from(["one", "two", "three"]);
1✔
320
        assert_eq!(actual_array.names(), &expected_names);
1✔
321

322
        assert_eq!(
1✔
323
            primitive_field(actual_array.as_ref(), &["one"])
1✔
324
                .unwrap()
1✔
325
                .as_slice::<i32>(),
1✔
326
            [0, 1, 2]
1✔
327
        );
1✔
328
        assert_eq!(
1✔
329
            primitive_field(actual_array.as_ref(), &["two", "two_one"])
1✔
330
                .unwrap()
1✔
331
                .as_slice::<i32>(),
1✔
332
            [4, 5, 6]
1✔
333
        );
1✔
334
        assert_eq!(
1✔
335
            primitive_field(actual_array.as_ref(), &["two", "two_two"])
1✔
336
                .unwrap()
1✔
337
                .as_slice::<i32>(),
1✔
338
            [4, 5, 6]
1✔
339
        );
1✔
340
        assert_eq!(
1✔
341
            primitive_field(actual_array.as_ref(), &["three"])
1✔
342
                .unwrap()
1✔
343
                .as_slice::<i32>(),
1✔
344
            [0, 1, 2]
1✔
345
        );
1✔
346
    }
1✔
347

348
    #[test]
349
    pub fn test_pack_nullable() {
1✔
350
        let expr = Pack::try_new_expr(
1✔
351
            ["one", "two", "three"].into(),
1✔
352
            vec![col("a"), col("b"), col("a")],
1✔
353
            Nullability::Nullable,
1✔
354
        )
1✔
355
        .unwrap();
1✔
356

1✔
357
        let actual_array = expr
1✔
358
            .evaluate(&Scope::new(test_array()))
1✔
359
            .unwrap()
1✔
360
            .to_struct()
1✔
361
            .unwrap();
1✔
362
        let expected_names: FieldNames = ["one", "two", "three"].into();
1✔
363
        assert_eq!(actual_array.names(), &expected_names);
1✔
364
        assert_eq!(actual_array.validity(), &Validity::AllValid);
1✔
365
    }
1✔
366
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc