Skip to main content

vortex_array/scalar_fn/fns/
select.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::fmt::Display;
5use std::fmt::Formatter;
6
7use itertools::Itertools;
8use prost::Message;
9use vortex_error::VortexExpect;
10use vortex_error::VortexResult;
11use vortex_error::vortex_bail;
12use vortex_error::vortex_err;
13use vortex_proto::expr::FieldNames as ProtoFieldNames;
14use vortex_proto::expr::SelectOpts;
15use vortex_proto::expr::select_opts::Opts;
16use vortex_session::VortexSession;
17use vortex_session::registry::CachedId;
18
19use crate::ArrayRef;
20use crate::ExecutionCtx;
21use crate::IntoArray;
22use crate::arrays::Constant;
23use crate::arrays::ConstantArray;
24use crate::arrays::StructArray;
25use crate::arrays::struct_::StructArrayExt;
26use crate::dtype::DType;
27use crate::dtype::FieldName;
28use crate::dtype::FieldNames;
29use crate::expr::display::ExprDisplay;
30use crate::expr::expression::Expression;
31use crate::expr::field::DisplayFieldNames;
32use crate::expr::get_item;
33use crate::expr::pack;
34use crate::scalar_fn::Arity;
35use crate::scalar_fn::ChildName;
36use crate::scalar_fn::ExecutionArgs;
37use crate::scalar_fn::ScalarFnId;
38use crate::scalar_fn::ScalarFnVTable;
39use crate::scalar_fn::SimplifyCtx;
40use crate::scalar_fn::fns::pack::Pack;
41
42#[derive(Debug, Clone, PartialEq, Eq, Hash)]
43pub enum FieldSelection {
44    Include(FieldNames),
45    Exclude(FieldNames),
46}
47
48#[derive(Clone)]
49pub struct Select;
50
51impl ScalarFnVTable for Select {
52    type Options = FieldSelection;
53
54    fn id(&self) -> ScalarFnId {
55        static ID: CachedId = CachedId::new("vortex.select");
56        *ID
57    }
58
59    fn serialize(&self, instance: &FieldSelection) -> VortexResult<Option<Vec<u8>>> {
60        let opts = match instance {
61            FieldSelection::Include(fields) => Opts::Include(ProtoFieldNames {
62                names: fields.iter().map(|f| f.to_string()).collect(),
63            }),
64            FieldSelection::Exclude(fields) => Opts::Exclude(ProtoFieldNames {
65                names: fields.iter().map(|f| f.to_string()).collect(),
66            }),
67        };
68
69        let select_opts = SelectOpts { opts: Some(opts) };
70        Ok(Some(select_opts.encode_to_vec()))
71    }
72
73    fn deserialize(
74        &self,
75        _metadata: &[u8],
76        _session: &VortexSession,
77    ) -> VortexResult<FieldSelection> {
78        let prost_metadata = SelectOpts::decode(_metadata)?;
79
80        let select_opts = prost_metadata
81            .opts
82            .ok_or_else(|| vortex_err!("SelectOpts missing opts field"))?;
83
84        let field_selection = match select_opts {
85            Opts::Include(field_names) => FieldSelection::Include(FieldNames::from_iter(
86                field_names.names.iter().map(|s| s.as_str()),
87            )),
88            Opts::Exclude(field_names) => FieldSelection::Exclude(FieldNames::from_iter(
89                field_names.names.iter().map(|s| s.as_str()),
90            )),
91        };
92
93        Ok(field_selection)
94    }
95
96    fn arity(&self, _options: &FieldSelection) -> Arity {
97        Arity::Exact(1)
98    }
99
100    fn child_name(&self, _instance: &FieldSelection, child_idx: usize) -> ChildName {
101        match child_idx {
102            0 => ChildName::from("child"),
103            _ => unreachable!(),
104        }
105    }
106
107    fn fmt_sql(
108        &self,
109        selection: &FieldSelection,
110        expr: &dyn ExprDisplay,
111        f: &mut Formatter<'_>,
112    ) -> std::fmt::Result {
113        Display::fmt(expr.display_child(0), f)?;
114        match selection {
115            FieldSelection::Include(fields) => {
116                write!(f, "{{{}}}", DisplayFieldNames(fields))
117            }
118            FieldSelection::Exclude(fields) => {
119                write!(f, "{{~ {}}}", DisplayFieldNames(fields))
120            }
121        }
122    }
123
124    fn return_dtype(
125        &self,
126        selection: &FieldSelection,
127        arg_dtypes: &[DType],
128    ) -> VortexResult<DType> {
129        let child_dtype = &arg_dtypes[0];
130        let child_struct_dtype = child_dtype
131            .as_struct_fields_opt()
132            .ok_or_else(|| vortex_err!("Select child not a struct dtype"))?;
133
134        let projected = match selection {
135            FieldSelection::Include(fields) => child_struct_dtype.project(fields.as_ref())?,
136            FieldSelection::Exclude(fields) => child_struct_dtype
137                .names()
138                .iter()
139                .cloned()
140                .zip_eq(child_struct_dtype.fields())
141                .filter(|(name, _)| !fields.as_ref().contains(name))
142                .collect(),
143        };
144
145        Ok(DType::Struct(projected, child_dtype.nullability()))
146    }
147
148    fn execute(
149        &self,
150        selection: &FieldSelection,
151        args: &dyn ExecutionArgs,
152        ctx: &mut ExecutionCtx,
153    ) -> VortexResult<ArrayRef> {
154        let child = args.get(0)?;
155        if let Some(constant) = child.as_opt::<Constant>() {
156            let child_struct_dtype = child
157                .dtype()
158                .as_struct_fields_opt()
159                .ok_or_else(|| vortex_err!("Select child not a struct dtype"))?;
160            let included = selection.normalize_to_included_fields(child_struct_dtype.names())?;
161            let scalar = constant.scalar().as_struct().project(included.as_ref())?;
162
163            return Ok(ConstantArray::new(scalar, child.len()).into_array());
164        }
165
166        let child = child.execute::<StructArray>(ctx)?;
167
168        let result = match selection {
169            FieldSelection::Include(f) => child.project(f.as_ref()),
170            FieldSelection::Exclude(names) => {
171                let included_names = child
172                    .names()
173                    .iter()
174                    .filter(|&f| !names.as_ref().contains(f))
175                    .cloned()
176                    .collect::<Vec<_>>();
177                child.project(included_names.as_slice())
178            }
179        }?;
180
181        result.into_array().execute(ctx)
182    }
183
184    fn simplify(
185        &self,
186        selection: &FieldSelection,
187        expr: &Expression,
188        ctx: &dyn SimplifyCtx,
189    ) -> VortexResult<Option<Expression>> {
190        let child_struct = expr.child(0);
191        let struct_dtype = ctx.return_dtype(child_struct)?;
192        let struct_nullability = struct_dtype.nullability();
193
194        let struct_fields = struct_dtype.as_struct_fields_opt().ok_or_else(|| {
195            vortex_err!(
196                "Select child must return a struct dtype, however it was a {}",
197                struct_dtype
198            )
199        })?;
200
201        // "Mask" out the unwanted fields of the child struct `DType`.
202        let included_fields = selection.normalize_to_included_fields(struct_fields.names())?;
203        let all_included_fields_are_nullable = included_fields.iter().all(|name| {
204            struct_fields
205                .field(name)
206                .vortex_expect(
207                    "`normalize_to_included_fields` checks that the included fields already exist \
208                     in `struct_fields`",
209                )
210                .is_nullable()
211        });
212
213        // If no fields are included, we can trivially simplify to a pack expression.
214        // NOTE(ngates): we do this knowing that our layout expression partitioning logic has
215        //  special-casing for pack, but not for select. We will fix this up when we revisit the
216        //  layout APIs.
217        if included_fields.is_empty() {
218            let empty: Vec<(FieldName, Expression)> = vec![];
219            return Ok(Some(pack(empty, struct_nullability)));
220        }
221
222        // We cannot always convert a `select` into a `pack(get_item(f1), get_item(f2), ...)`.
223        // This is because `get_item` does a validity intersection of the struct validity with its
224        // fields, which is not the same as just "masking" out the unwanted fields (a selection).
225        //
226        // We can, however, make this simplification when the child of the `select` is already a
227        // `pack` and we know that `get_item` will do no validity intersections.
228        let child_is_pack = child_struct.is::<Pack>();
229
230        // `get_item` only performs validity intersection when the struct is nullable but the field
231        // is not. This would change the semantics of a `select`, so we can only simplify when this
232        // won't happen.
233        let would_intersect_validity =
234            struct_nullability.is_nullable() && !all_included_fields_are_nullable;
235
236        if child_is_pack && !would_intersect_validity {
237            let pack_expr = pack(
238                included_fields
239                    .into_iter()
240                    .map(|name| (name.clone(), get_item(name, child_struct.clone()))),
241                struct_nullability,
242            );
243
244            return Ok(Some(pack_expr));
245        }
246
247        Ok(None)
248    }
249
250    fn is_strict(&self, _options: &FieldSelection) -> bool {
251        true
252    }
253
254    fn is_infallible(&self, _instance: &FieldSelection) -> bool {
255        // If this type-checks, it is infallible.
256        true
257    }
258}
259
260impl FieldSelection {
261    pub fn include(columns: FieldNames) -> Self {
262        assert_eq!(columns.iter().unique().collect_vec().len(), columns.len());
263        Self::Include(columns)
264    }
265
266    pub fn exclude(columns: FieldNames) -> Self {
267        assert_eq!(columns.iter().unique().collect_vec().len(), columns.len());
268        Self::Exclude(columns)
269    }
270
271    pub fn is_include(&self) -> bool {
272        matches!(self, Self::Include(_))
273    }
274
275    pub fn is_exclude(&self) -> bool {
276        matches!(self, Self::Exclude(_))
277    }
278
279    pub fn field_names(&self) -> &FieldNames {
280        let (FieldSelection::Include(fields) | FieldSelection::Exclude(fields)) = self;
281
282        fields
283    }
284
285    pub fn normalize_to_included_fields(
286        &self,
287        available_fields: &FieldNames,
288    ) -> VortexResult<FieldNames> {
289        // Check that all of the field names exist in the available fields.
290        if self
291            .field_names()
292            .iter()
293            .any(|f| !available_fields.iter().contains(f))
294        {
295            vortex_bail!(
296                "Select fields {:?} must be a subset of child fields {:?}",
297                self,
298                available_fields
299            );
300        }
301
302        match self {
303            FieldSelection::Include(fields) => Ok(fields.clone()),
304            FieldSelection::Exclude(exc_fields) => Ok(available_fields
305                .iter()
306                .filter(|f| !exc_fields.iter().contains(f))
307                .cloned()
308                .collect()),
309        }
310    }
311}
312
313impl Display for FieldSelection {
314    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
315        match self {
316            FieldSelection::Include(fields) => write!(f, "{{{}}}", DisplayFieldNames(fields)),
317            FieldSelection::Exclude(fields) => write!(f, "~{{{}}}", DisplayFieldNames(fields)),
318        }
319    }
320}
321
322#[cfg(test)]
323mod tests {
324    use vortex_buffer::buffer;
325    use vortex_error::VortexResult;
326
327    use crate::ArrayRef;
328    use crate::IntoArray;
329    use crate::VortexSessionExecute;
330    use crate::array_session;
331    use crate::arrays::Constant;
332    use crate::arrays::ConstantArray;
333    use crate::arrays::ScalarFnArray;
334    use crate::arrays::struct_::StructArrayExt;
335    use crate::dtype::DType;
336    use crate::dtype::FieldName;
337    use crate::dtype::FieldNames;
338    use crate::dtype::Nullability;
339    use crate::dtype::Nullability::Nullable;
340    use crate::dtype::PType::I32;
341    use crate::dtype::StructFields;
342    use crate::expr::root;
343    use crate::expr::select;
344    use crate::expr::select_exclude;
345    use crate::expr::test_harness;
346    use crate::scalar::Scalar;
347    use crate::scalar_fn::ScalarFnVTableExt;
348    use crate::scalar_fn::fns::select::FieldSelection;
349    use crate::scalar_fn::fns::select::Select;
350    use crate::scalar_fn::fns::select::StructArray;
351
352    fn test_array() -> StructArray {
353        StructArray::from_fields(&[
354            ("a", buffer![0, 1, 2].into_array()),
355            ("b", buffer![4, 5, 6].into_array()),
356        ])
357        .unwrap()
358    }
359
360    #[test]
361    pub fn include_columns() {
362        let mut ctx = array_session().create_execution_ctx();
363        let st = test_array();
364        let select = select(vec![FieldName::from("a")], root());
365        let selected = st
366            .into_array()
367            .apply(&select)
368            .unwrap()
369            .execute::<StructArray>(&mut ctx)
370            .unwrap();
371        let selected_names = selected.names().clone();
372        assert_eq!(selected_names.as_ref(), &["a"]);
373    }
374
375    #[test]
376    pub fn exclude_columns() {
377        let mut ctx = array_session().create_execution_ctx();
378        let st = test_array();
379        let select = select_exclude(vec![FieldName::from("a")], root());
380        let selected = st
381            .into_array()
382            .apply(&select)
383            .unwrap()
384            .execute::<StructArray>(&mut ctx)
385            .unwrap();
386        let selected_names = selected.names().clone();
387        assert_eq!(selected_names.as_ref(), &["b"]);
388    }
389
390    #[test]
391    fn dtype() {
392        let dtype = test_harness::struct_dtype();
393
394        let select_expr = select(vec![FieldName::from("a")], root());
395        let expected_dtype = DType::Struct(
396            dtype
397                .as_struct_fields_opt()
398                .unwrap()
399                .project(&["a".into()])
400                .unwrap(),
401            Nullability::NonNullable,
402        );
403        assert_eq!(select_expr.return_dtype(&dtype).unwrap(), expected_dtype);
404
405        let select_expr_exclude = select_exclude(
406            vec![
407                FieldName::from("col1"),
408                FieldName::from("col2"),
409                FieldName::from("bool1"),
410                FieldName::from("bool2"),
411            ],
412            root(),
413        );
414        assert_eq!(
415            select_expr_exclude.return_dtype(&dtype).unwrap(),
416            expected_dtype
417        );
418
419        let select_expr_exclude = select_exclude(
420            vec![FieldName::from("col1"), FieldName::from("col2")],
421            root(),
422        );
423        assert_eq!(
424            select_expr_exclude.return_dtype(&dtype).unwrap(),
425            DType::Struct(
426                dtype
427                    .as_struct_fields_opt()
428                    .unwrap()
429                    .project(&["a".into(), "bool1".into(), "bool2".into()])
430                    .unwrap(),
431                Nullability::NonNullable
432            )
433        );
434    }
435
436    #[test]
437    fn test_as_include_names() {
438        let field_names = FieldNames::from(["a", "b", "c"]);
439        let include = select(["a"], root());
440        let exclude = select_exclude(["b", "c"], root());
441        assert_eq!(
442            &include
443                .as_::<Select>()
444                .normalize_to_included_fields(&field_names)
445                .unwrap(),
446            &exclude
447                .as_::<Select>()
448                .normalize_to_included_fields(&field_names)
449                .unwrap()
450        );
451    }
452
453    #[test]
454    fn execute_constant_struct_stays_constant() -> VortexResult<()> {
455        let field_count = 128usize;
456        let names = (0..field_count)
457            .map(|idx| FieldName::from(format!("f{idx}")))
458            .collect::<Vec<_>>();
459        let dtypes = vec![I32.into(); field_count];
460        let fields = StructFields::new(FieldNames::from(names), dtypes);
461        let scalar = Scalar::struct_(
462            DType::Struct(fields, Nullability::NonNullable),
463            (0..128).map(Scalar::from),
464        );
465        let array = ConstantArray::new(scalar, 1_000_000).into_array();
466        let mut ctx = array_session().create_execution_ctx();
467
468        let item: ArrayRef = ScalarFnArray::try_new(
469            Select.bind(FieldSelection::Include(FieldNames::from(["f97", "f3"]))),
470            vec![array],
471        )?
472        .into_array()
473        .execute(&mut ctx)?;
474
475        let constant = item.as_::<Constant>();
476        assert_eq!(constant.len(), 1_000_000);
477        assert_eq!(
478            constant.scalar(),
479            &Scalar::struct_(
480                DType::Struct(
481                    StructFields::new(
482                        FieldNames::from(["f97", "f3"]),
483                        vec![I32.into(), I32.into()],
484                    ),
485                    Nullability::NonNullable,
486                ),
487                [Scalar::from(97), Scalar::from(3)],
488            )
489        );
490        Ok(())
491    }
492
493    #[test]
494    fn test_remove_select_rule() {
495        let dtype = DType::Struct(
496            StructFields::new(["a", "b"].into(), vec![I32.into(), I32.into()]),
497            Nullable,
498        );
499        let e = select(["a", "b"], root());
500
501        let result = e.optimize_recursive(&dtype).unwrap();
502
503        assert!(result.return_dtype(&dtype).unwrap().is_nullable());
504    }
505
506    #[test]
507    fn test_remove_select_rule_exclude_fields() {
508        use crate::expr::select_exclude;
509
510        let dtype = DType::Struct(
511            StructFields::new(
512                ["a", "b", "c"].into(),
513                vec![I32.into(), I32.into(), I32.into()],
514            ),
515            Nullable,
516        );
517        let e = select_exclude(["c"], root());
518
519        let result = e.optimize_recursive(&dtype).unwrap();
520
521        // Should exclude "c" and include "a" and "b"
522        let result_dtype = result.return_dtype(&dtype).unwrap();
523        assert!(result_dtype.is_nullable());
524        let fields = result_dtype.as_struct_fields_opt().unwrap();
525        assert_eq!(fields.names().as_ref(), &["a", "b"]);
526    }
527}