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