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

vortex-data / vortex / 16653606492

31 Jul 2025 03:42PM UTC coverage: 83.029% (-0.02%) from 83.045%
16653606492

push

github

web-flow
feat: Add serialization support for Select expression (#4077)

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

3 of 16 new or added lines in 2 files covered. (18.75%)

1 existing line in 1 file now uncovered.

46014 of 55419 relevant lines covered (83.03%)

452831.93 hits per line

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

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

4
use std::fmt::Display;
5

6
use itertools::Itertools;
7
use vortex_array::{ArrayRef, DeserializeMetadata, IntoArray, ProstMetadata, ToCanonical};
8
use vortex_dtype::{DType, FieldNames};
9
use vortex_error::{VortexResult, vortex_bail, vortex_err};
10
use vortex_proto::expr::select_opts::Opts;
11
use vortex_proto::expr::{FieldNames as ProtoFieldNames, SelectOpts};
12

13
use crate::field::DisplayFieldNames;
14
use crate::{AnalysisExpr, ExprEncodingRef, ExprId, ExprRef, IntoExpr, Scope, VTable, vtable};
15

16
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
17
pub enum SelectField {
18
    Include(FieldNames),
19
    Exclude(FieldNames),
20
}
21

22
vtable!(Select);
23

24
#[derive(Debug, Clone, Hash)]
25
#[allow(clippy::derived_hash_with_manual_eq)]
26
pub struct SelectExpr {
27
    fields: SelectField,
28
    child: ExprRef,
29
}
30

31
impl PartialEq for SelectExpr {
32
    fn eq(&self, other: &Self) -> bool {
×
33
        self.fields == other.fields && self.child.eq(&other.child)
×
34
    }
×
35
}
36

37
pub struct SelectExprEncoding;
38

39
impl VTable for SelectVTable {
40
    type Expr = SelectExpr;
41
    type Encoding = SelectExprEncoding;
42
    type Metadata = ProstMetadata<SelectOpts>;
43

44
    fn id(_encoding: &Self::Encoding) -> ExprId {
128✔
45
        ExprId::new_ref("select")
128✔
46
    }
128✔
47

48
    fn encoding(_expr: &Self::Expr) -> ExprEncodingRef {
×
49
        ExprEncodingRef::new_ref(SelectExprEncoding.as_ref())
×
50
    }
×
51

NEW
52
    fn metadata(expr: &Self::Expr) -> Option<Self::Metadata> {
×
NEW
53
        let names = expr
×
NEW
54
            .fields()
×
NEW
55
            .fields()
×
NEW
56
            .iter()
×
NEW
57
            .map(|f| f.to_string())
×
NEW
58
            .collect_vec();
×
59

NEW
60
        let opts = if expr.fields().is_include() {
×
NEW
61
            Opts::Include(ProtoFieldNames { names })
×
62
        } else {
NEW
63
            Opts::Exclude(ProtoFieldNames { names })
×
64
        };
65

NEW
66
        Some(ProstMetadata(SelectOpts { opts: Some(opts) }))
×
UNCOV
67
    }
×
68

69
    fn children(expr: &Self::Expr) -> Vec<&ExprRef> {
3,037✔
70
        vec![&expr.child]
3,037✔
71
    }
3,037✔
72

73
    fn with_children(expr: &Self::Expr, children: Vec<ExprRef>) -> VortexResult<Self::Expr> {
×
74
        Ok(SelectExpr {
×
75
            fields: expr.fields.clone(),
×
76
            child: children[0].clone(),
×
77
        })
×
78
    }
×
79

80
    fn build(
×
81
        _encoding: &Self::Encoding,
×
82
        _metadata: &<Self::Metadata as DeserializeMetadata>::Output,
×
83
        _children: Vec<ExprRef>,
×
84
    ) -> VortexResult<Self::Expr> {
×
85
        vortex_bail!("Select does not support deserialization")
×
86
    }
×
87

88
    fn evaluate(expr: &Self::Expr, scope: &Scope) -> VortexResult<ArrayRef> {
2✔
89
        let batch = expr.child.unchecked_evaluate(scope)?.to_struct()?;
2✔
90
        Ok(match &expr.fields {
2✔
91
            SelectField::Include(f) => batch.project(f.as_ref()),
1✔
92
            SelectField::Exclude(names) => {
1✔
93
                let included_names = batch
1✔
94
                    .names()
1✔
95
                    .iter()
1✔
96
                    .filter(|&f| !names.as_ref().contains(f))
2✔
97
                    .cloned()
1✔
98
                    .collect::<Vec<_>>();
1✔
99
                batch.project(included_names.as_slice())
1✔
100
            }
101
        }?
×
102
        .into_array())
2✔
103
    }
2✔
104

105
    fn return_dtype(expr: &Self::Expr, scope: &DType) -> VortexResult<DType> {
68✔
106
        let child_dtype = expr.child.return_dtype(scope)?;
68✔
107
        let child_struct_dtype = child_dtype
68✔
108
            .as_struct()
68✔
109
            .ok_or_else(|| vortex_err!("Select child not a struct dtype"))?;
68✔
110

111
        let projected = match &expr.fields {
68✔
112
            SelectField::Include(fields) => child_struct_dtype.project(fields.as_ref())?,
65✔
113
            SelectField::Exclude(fields) => child_struct_dtype
3✔
114
                .names()
3✔
115
                .iter()
3✔
116
                .cloned()
3✔
117
                .zip_eq(child_struct_dtype.fields())
3✔
118
                .filter(|(name, _)| !fields.as_ref().contains(name))
12✔
119
                .collect(),
3✔
120
        };
121

122
        Ok(DType::Struct(projected, child_dtype.nullability()))
68✔
123
    }
68✔
124
}
125

126
pub fn select(fields: impl Into<FieldNames>, child: ExprRef) -> ExprRef {
676✔
127
    SelectExpr::include_expr(fields.into(), child)
676✔
128
}
676✔
129

130
pub fn select_exclude(fields: impl Into<FieldNames>, child: ExprRef) -> ExprRef {
4✔
131
    SelectExpr::exclude_expr(fields.into(), child)
4✔
132
}
4✔
133

134
impl SelectExpr {
135
    pub fn new(fields: SelectField, child: ExprRef) -> Self {
1,000✔
136
        Self { fields, child }
1,000✔
137
    }
1,000✔
138

139
    pub fn new_expr(fields: SelectField, child: ExprRef) -> ExprRef {
×
140
        Self::new(fields, child).into_expr()
×
141
    }
×
142

143
    pub fn include_expr(columns: FieldNames, child: ExprRef) -> ExprRef {
996✔
144
        Self::new(SelectField::Include(columns), child).into_expr()
996✔
145
    }
996✔
146

147
    pub fn exclude_expr(columns: FieldNames, child: ExprRef) -> ExprRef {
4✔
148
        Self::new(SelectField::Exclude(columns), child).into_expr()
4✔
149
    }
4✔
150

151
    pub fn fields(&self) -> &SelectField {
1,013✔
152
        &self.fields
1,013✔
153
    }
1,013✔
154

155
    pub fn child(&self) -> &ExprRef {
1,013✔
156
        &self.child
1,013✔
157
    }
1,013✔
158

159
    pub fn as_include(&self, field_names: &FieldNames) -> VortexResult<ExprRef> {
×
160
        Ok(Self::new(
×
161
            SelectField::Include(self.fields.as_include_names(field_names)?),
×
162
            self.child.clone(),
×
163
        )
164
        .into_expr())
×
165
    }
×
166
}
167

168
impl SelectField {
169
    pub fn include(columns: FieldNames) -> Self {
×
170
        assert_eq!(columns.iter().unique().collect_vec().len(), columns.len());
×
171
        Self::Include(columns)
×
172
    }
×
173

174
    pub fn exclude(columns: FieldNames) -> Self {
×
175
        assert_eq!(columns.iter().unique().collect_vec().len(), columns.len());
×
176
        Self::Exclude(columns)
×
177
    }
×
178

179
    pub fn is_include(&self) -> bool {
×
180
        matches!(self, Self::Include(_))
×
181
    }
×
182

183
    pub fn is_exclude(&self) -> bool {
×
184
        matches!(self, Self::Exclude(_))
×
185
    }
×
186

187
    pub fn fields(&self) -> &FieldNames {
1,013✔
188
        let (SelectField::Include(fields) | SelectField::Exclude(fields)) = self;
1,013✔
189

190
        fields
1,013✔
191
    }
1,013✔
192

193
    pub fn as_include_names(&self, field_names: &FieldNames) -> VortexResult<FieldNames> {
1,013✔
194
        if self
1,013✔
195
            .fields()
1,013✔
196
            .iter()
1,013✔
197
            .any(|f| !field_names.iter().contains(f))
2,248✔
198
        {
199
            vortex_bail!(
×
200
                "Field {:?} in select not in field names {:?}",
×
201
                self,
202
                field_names
203
            );
204
        }
1,013✔
205
        match self {
1,013✔
206
            SelectField::Include(fields) => Ok(fields.clone()),
1,013✔
207
            SelectField::Exclude(exc_fields) => Ok(field_names
×
208
                .iter()
×
209
                .filter(|f| exc_fields.iter().contains(f))
×
210
                .cloned()
×
211
                .collect()),
×
212
        }
213
    }
1,013✔
214
}
215

216
impl Display for SelectField {
217
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3✔
218
        match self {
3✔
219
            SelectField::Include(fields) => write!(f, "{{{}}}", DisplayFieldNames(fields)),
2✔
220
            SelectField::Exclude(fields) => write!(f, "~{{{}}}", DisplayFieldNames(fields)),
1✔
221
        }
222
    }
3✔
223
}
224

225
impl Display for SelectExpr {
226
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3✔
227
        write!(f, "{}{}", self.child, self.fields)
3✔
228
    }
3✔
229
}
230

231
impl AnalysisExpr for SelectExpr {}
232

233
#[cfg(test)]
234
mod tests {
235

236
    use vortex_array::arrays::StructArray;
237
    use vortex_array::{IntoArray, ToCanonical};
238
    use vortex_buffer::buffer;
239
    use vortex_dtype::{DType, FieldName, Nullability};
240

241
    use crate::{Scope, root, select, select_exclude, test_harness};
242

243
    fn test_array() -> StructArray {
2✔
244
        StructArray::from_fields(&[
2✔
245
            ("a", buffer![0, 1, 2].into_array()),
2✔
246
            ("b", buffer![4, 5, 6].into_array()),
2✔
247
        ])
2✔
248
        .unwrap()
2✔
249
    }
2✔
250

251
    #[test]
252
    pub fn include_columns() {
1✔
253
        let st = test_array();
1✔
254
        let select = select(vec![FieldName::from("a")], root());
1✔
255
        let selected = select
1✔
256
            .evaluate(&Scope::new(st.to_array()))
1✔
257
            .unwrap()
1✔
258
            .to_struct()
1✔
259
            .unwrap();
1✔
260
        let selected_names = selected.names().clone();
1✔
261
        assert_eq!(selected_names.as_ref(), &["a".into()]);
1✔
262
    }
1✔
263

264
    #[test]
265
    pub fn exclude_columns() {
1✔
266
        let st = test_array();
1✔
267
        let select = select_exclude(vec![FieldName::from("a")], root());
1✔
268
        let selected = select
1✔
269
            .evaluate(&Scope::new(st.to_array()))
1✔
270
            .unwrap()
1✔
271
            .to_struct()
1✔
272
            .unwrap();
1✔
273
        let selected_names = selected.names().clone();
1✔
274
        assert_eq!(selected_names.as_ref(), &["b".into()]);
1✔
275
    }
1✔
276

277
    #[test]
278
    fn dtype() {
1✔
279
        let dtype = test_harness::struct_dtype();
1✔
280

281
        let select_expr = select(vec![FieldName::from("a")], root());
1✔
282
        let expected_dtype = DType::Struct(
1✔
283
            dtype.as_struct().unwrap().project(&["a".into()]).unwrap(),
1✔
284
            Nullability::NonNullable,
1✔
285
        );
1✔
286
        assert_eq!(select_expr.return_dtype(&dtype).unwrap(), expected_dtype);
1✔
287

288
        let select_expr_exclude = select_exclude(
1✔
289
            vec![
1✔
290
                FieldName::from("col1"),
1✔
291
                FieldName::from("col2"),
1✔
292
                FieldName::from("bool1"),
1✔
293
                FieldName::from("bool2"),
1✔
294
            ],
295
            root(),
1✔
296
        );
297
        assert_eq!(
1✔
298
            select_expr_exclude.return_dtype(&dtype).unwrap(),
1✔
299
            expected_dtype
300
        );
301

302
        let select_expr_exclude = select_exclude(
1✔
303
            vec![FieldName::from("col1"), FieldName::from("col2")],
1✔
304
            root(),
1✔
305
        );
306
        assert_eq!(
1✔
307
            select_expr_exclude.return_dtype(&dtype).unwrap(),
1✔
308
            DType::Struct(
1✔
309
                dtype
1✔
310
                    .as_struct()
1✔
311
                    .unwrap()
1✔
312
                    .project(&["a".into(), "bool1".into(), "bool2".into()])
1✔
313
                    .unwrap(),
1✔
314
                Nullability::NonNullable
1✔
315
            )
1✔
316
        );
317
    }
1✔
318
}
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