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