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

vortex-data / vortex / 16935267080

13 Aug 2025 11:00AM UTC coverage: 24.312% (-63.3%) from 87.658%
16935267080

Pull #4226

github

web-flow
Merge 81b48c7fb into baa6ea202
Pull Request #4226: Support converting TimestampTZ to and from duckdb

0 of 2 new or added lines in 1 file covered. (0.0%)

20666 existing lines in 469 files now uncovered.

8726 of 35892 relevant lines covered (24.31%)

147.74 hits per line

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

44.54
/vortex-expr/src/exprs/pack.rs
1
// SPDX-License-Identifier: Apache-2.0
2
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3

4
use std::fmt::Display;
5
use std::hash::Hash;
6

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

15
use crate::{AnalysisExpr, ExprEncodingRef, ExprId, ExprRef, IntoExpr, Scope, VTable, vtable};
16

17
vtable!(Pack);
18

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

55
pub struct PackExprEncoding;
56

57
impl VTable for PackVTable {
58
    type Expr = PackExpr;
59
    type Encoding = PackExprEncoding;
60
    type Metadata = ProstMetadata<pb::PackOpts>;
61

UNCOV
62
    fn id(_encoding: &Self::Encoding) -> ExprId {
×
UNCOV
63
        ExprId::new_ref("pack")
×
UNCOV
64
    }
×
65

UNCOV
66
    fn encoding(_expr: &Self::Expr) -> ExprEncodingRef {
×
UNCOV
67
        ExprEncodingRef::new_ref(PackExprEncoding.as_ref())
×
UNCOV
68
    }
×
69

UNCOV
70
    fn metadata(expr: &Self::Expr) -> Option<Self::Metadata> {
×
71
        Some(ProstMetadata(pb::PackOpts {
UNCOV
72
            paths: expr.names.iter().map(|n| n.to_string()).collect(),
×
UNCOV
73
            nullable: expr.nullability.into(),
×
74
        }))
UNCOV
75
    }
×
76

77
    fn children(expr: &Self::Expr) -> Vec<&ExprRef> {
56✔
78
        expr.values.iter().collect()
56✔
79
    }
56✔
80

81
    fn with_children(expr: &Self::Expr, children: Vec<ExprRef>) -> VortexResult<Self::Expr> {
6✔
82
        PackExpr::try_new(expr.names.clone(), children, expr.nullability)
6✔
83
    }
6✔
84

UNCOV
85
    fn build(
×
UNCOV
86
        _encoding: &Self::Encoding,
×
UNCOV
87
        metadata: &<Self::Metadata as DeserializeMetadata>::Output,
×
UNCOV
88
        children: Vec<ExprRef>,
×
UNCOV
89
    ) -> VortexResult<Self::Expr> {
×
UNCOV
90
        if children.len() != metadata.paths.len() {
×
91
            vortex_bail!(
×
92
                "Pack expression expects {} children, got {}",
×
93
                metadata.paths.len(),
×
94
                children.len()
×
95
            );
UNCOV
96
        }
×
UNCOV
97
        let names: FieldNames = metadata
×
UNCOV
98
            .paths
×
UNCOV
99
            .iter()
×
UNCOV
100
            .map(|name| FieldName::from(name.as_str()))
×
UNCOV
101
            .collect();
×
UNCOV
102
        PackExpr::try_new(names, children, metadata.nullable.into())
×
UNCOV
103
    }
×
104

105
    fn evaluate(expr: &Self::Expr, scope: &Scope) -> VortexResult<ArrayRef> {
6✔
106
        let len = scope.len();
6✔
107
        let value_arrays = expr
6✔
108
            .values
6✔
109
            .iter()
6✔
110
            .map(|value_expr| value_expr.unchecked_evaluate(scope))
8✔
111
            .process_results(|it| it.collect::<Vec<_>>())?;
6✔
112
        let validity = match expr.nullability {
6✔
113
            Nullability::NonNullable => Validity::NonNullable,
6✔
UNCOV
114
            Nullability::Nullable => Validity::AllValid,
×
115
        };
116
        Ok(StructArray::try_new(expr.names.clone(), value_arrays, len, validity)?.into_array())
6✔
117
    }
6✔
118

119
    fn return_dtype(expr: &Self::Expr, scope: &DType) -> VortexResult<DType> {
12✔
120
        let value_dtypes = expr
12✔
121
            .values
12✔
122
            .iter()
12✔
123
            .map(|value_expr| value_expr.return_dtype(scope))
14✔
124
            .process_results(|it| it.collect())?;
12✔
125
        Ok(DType::Struct(
12✔
126
            StructFields::new(expr.names.clone(), value_dtypes),
12✔
127
            expr.nullability,
12✔
128
        ))
12✔
129
    }
12✔
130
}
131

132
impl PackExpr {
133
    pub fn try_new(
14✔
134
        names: FieldNames,
14✔
135
        values: Vec<ExprRef>,
14✔
136
        nullability: Nullability,
14✔
137
    ) -> VortexResult<Self> {
14✔
138
        if names.len() != values.len() {
14✔
139
            vortex_bail!("length mismatch {} {}", names.len(), values.len());
×
140
        }
14✔
141
        Ok(PackExpr {
14✔
142
            names,
14✔
143
            values,
14✔
144
            nullability,
14✔
145
        })
14✔
146
    }
14✔
147

UNCOV
148
    pub fn try_new_expr(
×
UNCOV
149
        names: FieldNames,
×
UNCOV
150
        values: Vec<ExprRef>,
×
UNCOV
151
        nullability: Nullability,
×
UNCOV
152
    ) -> VortexResult<ExprRef> {
×
UNCOV
153
        Self::try_new(names, values, nullability).map(|v| v.into_expr())
×
UNCOV
154
    }
×
155

156
    pub fn names(&self) -> &FieldNames {
×
157
        &self.names
×
158
    }
×
159

UNCOV
160
    pub fn field(&self, field_name: &FieldName) -> VortexResult<ExprRef> {
×
UNCOV
161
        let idx = self
×
UNCOV
162
            .names
×
UNCOV
163
            .iter()
×
UNCOV
164
            .position(|name| name == field_name)
×
UNCOV
165
            .ok_or_else(|| {
×
166
                vortex_err!(
×
167
                    "Cannot find field {} in pack fields {:?}",
×
168
                    field_name,
169
                    self.names
170
                )
171
            })?;
×
172

UNCOV
173
        self.values
×
UNCOV
174
            .get(idx)
×
UNCOV
175
            .cloned()
×
UNCOV
176
            .ok_or_else(|| vortex_err!("field index out of bounds: {}", idx))
×
UNCOV
177
    }
×
178

179
    pub fn nullability(&self) -> Nullability {
×
180
        self.nullability
×
181
    }
×
182
}
183

184
pub fn pack(
8✔
185
    elements: impl IntoIterator<Item = (impl Into<FieldName>, ExprRef)>,
8✔
186
    nullability: Nullability,
8✔
187
) -> ExprRef {
8✔
188
    let (names, values): (Vec<_>, Vec<_>) = elements
8✔
189
        .into_iter()
8✔
190
        .map(|(name, value)| (name.into(), value))
10✔
191
        .unzip();
8✔
192
    PackExpr::try_new(names.into(), values, nullability)
8✔
193
        .vortex_expect("pack names and values have the same length")
8✔
194
        .into_expr()
8✔
195
}
8✔
196

197
impl Display for PackExpr {
UNCOV
198
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
UNCOV
199
        write!(
×
UNCOV
200
            f,
×
UNCOV
201
            "pack({}){}",
×
UNCOV
202
            self.names
×
UNCOV
203
                .iter()
×
UNCOV
204
                .zip(&self.values)
×
UNCOV
205
                .format_with(", ", |(name, expr), f| f(&format_args!("{name}: {expr}"))),
×
206
            self.nullability
207
        )
UNCOV
208
    }
×
209
}
210

211
impl AnalysisExpr for PackExpr {}
212

213
#[cfg(test)]
214
mod tests {
215

216
    use vortex_array::arrays::{PrimitiveArray, StructArray};
217
    use vortex_array::validity::Validity;
218
    use vortex_array::vtable::ValidityHelper;
219
    use vortex_array::{Array, ArrayRef, IntoArray, ToCanonical};
220
    use vortex_buffer::buffer;
221
    use vortex_dtype::{FieldNames, Nullability};
222
    use vortex_error::{VortexResult, vortex_bail};
223

224
    use crate::{IntoExpr, PackExpr, Scope, col};
225

226
    fn test_array() -> ArrayRef {
227
        StructArray::from_fields(&[
228
            ("a", buffer![0, 1, 2].into_array()),
229
            ("b", buffer![4, 5, 6].into_array()),
230
        ])
231
        .unwrap()
232
        .into_array()
233
    }
234

235
    fn primitive_field(array: &dyn Array, field_path: &[&str]) -> VortexResult<PrimitiveArray> {
236
        let mut field_path = field_path.iter();
237

238
        let Some(field) = field_path.next() else {
239
            vortex_bail!("empty field path");
240
        };
241

242
        let mut array = array.to_struct()?.field_by_name(field)?.clone();
243
        for field in field_path {
244
            array = array.to_struct()?.field_by_name(field)?.clone();
245
        }
246
        Ok(array.to_primitive().unwrap())
247
    }
248

249
    #[test]
250
    pub fn test_empty_pack() {
251
        let expr =
252
            PackExpr::try_new(FieldNames::default(), Vec::new(), Nullability::NonNullable).unwrap();
253

254
        let test_array = test_array();
255
        let actual_array = expr.evaluate(&Scope::new(test_array.clone())).unwrap();
256
        assert_eq!(actual_array.len(), test_array.len());
257
        assert_eq!(
258
            actual_array.to_struct().unwrap().struct_fields().nfields(),
259
            0
260
        );
261
    }
262

263
    #[test]
264
    pub fn test_simple_pack() {
265
        let expr = PackExpr::try_new(
266
            ["one", "two", "three"].into(),
267
            vec![col("a"), col("b"), col("a")],
268
            Nullability::NonNullable,
269
        )
270
        .unwrap();
271

272
        let actual_array = expr
273
            .evaluate(&Scope::new(test_array()))
274
            .unwrap()
275
            .to_struct()
276
            .unwrap();
277
        let expected_names: FieldNames = ["one", "two", "three"].into();
278
        assert_eq!(actual_array.names(), &expected_names);
279
        assert_eq!(actual_array.validity(), &Validity::NonNullable);
280

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

301
    #[test]
302
    pub fn test_nested_pack() {
303
        let expr = PackExpr::try_new(
304
            ["one", "two", "three"].into(),
305
            vec![
306
                col("a"),
307
                PackExpr::try_new(
308
                    ["two_one", "two_two"].into(),
309
                    vec![col("b"), col("b")],
310
                    Nullability::NonNullable,
311
                )
312
                .unwrap()
313
                .into_expr(),
314
                col("a"),
315
            ],
316
            Nullability::NonNullable,
317
        )
318
        .unwrap();
319

320
        let actual_array = expr
321
            .evaluate(&Scope::new(test_array()))
322
            .unwrap()
323
            .to_struct()
324
            .unwrap();
325
        let expected_names = FieldNames::from(["one", "two", "three"]);
326
        assert_eq!(actual_array.names(), &expected_names);
327

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

354
    #[test]
355
    pub fn test_pack_nullable() {
356
        let expr = PackExpr::try_new(
357
            ["one", "two", "three"].into(),
358
            vec![col("a"), col("b"), col("a")],
359
            Nullability::Nullable,
360
        )
361
        .unwrap();
362

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

© 2026 Coveralls, Inc