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