Skip to main content

nu_command/filters/
select.rs

1#[cfg(feature = "sqlite")]
2use crate::database::SQLiteQueryBuilder;
3use nu_engine::command_prelude::*;
4use nu_protocol::{
5    DeprecationEntry, DeprecationType, PipelineIterator, ReportMode, ast::PathMember,
6    casing::Casing,
7};
8use std::collections::BTreeSet;
9
10#[derive(Clone)]
11pub struct Select;
12
13impl Command for Select {
14    fn name(&self) -> &str {
15        "select"
16    }
17
18    // FIXME: also add support for --skip
19    fn signature(&self) -> Signature {
20        Signature::build("select")
21            .input_output_types(vec![
22                (Type::record(), Type::record()),
23                (Type::table(), Type::table()),
24                (Type::List(Box::new(Type::Any)), Type::Any),
25                #[cfg(feature = "sqlite")]
26                (
27                    Type::Custom("SQLiteQueryBuilder".into()),
28                    Type::Custom("SQLiteQueryBuilder".into()),
29                ),
30            ])
31            .switch(
32                "optional",
33                "Make all cell path members optional (returns `null` for missing values).",
34                Some('o'),
35            )
36            .switch(
37                "ignore-case",
38                "Make all cell path members case insensitive.",
39                None,
40            )
41            .switch(
42                "ignore-errors",
43                "Ignore missing data (make all cell path members optional) (deprecated).",
44                Some('i'),
45            )
46            .rest(
47                "rest",
48                SyntaxShape::CellPath,
49                "The columns to select from the table.",
50            )
51            .allow_variants_without_examples(true)
52            .category(Category::Filters)
53    }
54
55    fn description(&self) -> &str {
56        "Select only these columns or rows from the input. Opposite of `reject`."
57    }
58
59    fn extra_description(&self) -> &str {
60        r#"This differs from `get` in that, rather than accessing the given value in the data structure,
61it removes all non-selected values from the structure. Hence, using `select` on a table will
62produce a table, a list will produce a list, and a record will produce a record."#
63    }
64
65    fn search_terms(&self) -> Vec<&str> {
66        vec!["pick", "choose", "get", "retain"]
67    }
68
69    fn run(
70        &self,
71        engine_state: &EngineState,
72        stack: &mut Stack,
73        call: &Call,
74        input: PipelineData,
75    ) -> Result<PipelineData, ShellError> {
76        let columns: Vec<Value> = call.rest(engine_state, stack, 0)?;
77        let mut new_columns: Vec<CellPath> = vec![];
78        for col_val in columns {
79            let col_span = col_val.span();
80            match col_val {
81                Value::CellPath { val, .. } => {
82                    new_columns.push(val);
83                }
84                Value::String { val, .. } => {
85                    let cv = CellPath {
86                        members: vec![PathMember::String {
87                            val,
88                            span: col_span,
89                            optional: false,
90                            casing: Casing::Sensitive,
91                        }],
92                    };
93                    new_columns.push(cv);
94                }
95                Value::Int { val, .. } => {
96                    if val < 0 {
97                        return Err(ShellError::CantConvert {
98                            to_type: "cell path".into(),
99                            from_type: "negative number".into(),
100                            span: col_span,
101                            help: None,
102                        });
103                    }
104                    let cv = CellPath {
105                        members: vec![PathMember::Int {
106                            val: val as usize,
107                            span: col_span,
108                            optional: false,
109                        }],
110                    };
111                    new_columns.push(cv);
112                }
113                x => {
114                    return Err(ShellError::CantConvert {
115                        to_type: "cell path".into(),
116                        from_type: x.get_type().to_string(),
117                        span: x.span(),
118                        help: None,
119                    });
120                }
121            }
122        }
123        let optional = call.has_flag(engine_state, stack, "optional")?
124            || call.has_flag(engine_state, stack, "ignore-errors")?;
125        let ignore_case = call.has_flag(engine_state, stack, "ignore-case")?;
126        let span = call.head;
127
128        if optional {
129            for cell_path in &mut new_columns {
130                cell_path.make_optional();
131            }
132        }
133
134        if ignore_case {
135            for cell_path in &mut new_columns {
136                cell_path.make_insensitive();
137            }
138        }
139
140        select(engine_state, span, new_columns, input)
141    }
142
143    fn deprecation_info(&self) -> Vec<DeprecationEntry> {
144        vec![DeprecationEntry {
145            ty: DeprecationType::Flag("ignore-errors".into()),
146            report_mode: ReportMode::FirstUse,
147            since: Some("0.106.0".into()),
148            expected_removal: None,
149            help: Some(
150                "This flag has been renamed to `--optional (-o)` to better reflect its behavior."
151                    .into(),
152            ),
153        }]
154    }
155
156    fn examples(&self) -> Vec<Example<'_>> {
157        vec![
158            Example {
159                description: "Select a column in a table.",
160                example: "[{a: a b: b}] | select a",
161                result: Some(Value::test_list(vec![Value::test_record(record! {
162                    "a" => Value::test_string("a")
163                })])),
164            },
165            Example {
166                description: "Select a column even if some rows are missing that column.",
167                example: "[{a: a0 b: b0} {b: b1}] | select -o a",
168                result: Some(Value::test_list(vec![
169                    Value::test_record(record! {
170                        "a" => Value::test_string("a0")
171                    }),
172                    Value::test_record(record! {
173                        "a" => Value::test_nothing()
174                    }),
175                ])),
176            },
177            Example {
178                description: "Select a field in a record.",
179                example: "{a: a b: b} | select a",
180                result: Some(Value::test_record(record! {
181                    "a" => Value::test_string("a")
182                })),
183            },
184            Example {
185                description: "Select just the `name` column.",
186                example: "ls | select name",
187                result: None,
188            },
189            Example {
190                description: "Select the first four rows (this is the same as `first 4`).",
191                example: "ls | select 0 1 2 3",
192                result: None,
193            },
194            Example {
195                description: "Select multiple columns.",
196                example: "[[name type size]; [Cargo.toml toml 1kb] [Cargo.lock toml 2kb]] | select name type",
197                result: Some(Value::test_list(vec![
198                    Value::test_record(record! {
199                        "name" => Value::test_string("Cargo.toml"),
200                        "type" => Value::test_string("toml"),
201                    }),
202                    Value::test_record(record! {
203                        "name" => Value::test_string("Cargo.lock"),
204                        "type" => Value::test_string("toml")
205                    }),
206                ])),
207            },
208            Example {
209                description: "Select multiple columns by spreading a list.",
210                example: r#"let cols = [name type]; [[name type size]; [Cargo.toml toml 1kb] [Cargo.lock toml 2kb]] | select ...$cols"#,
211                result: Some(Value::test_list(vec![
212                    Value::test_record(record! {
213                        "name" => Value::test_string("Cargo.toml"),
214                        "type" => Value::test_string("toml")
215                    }),
216                    Value::test_record(record! {
217                        "name" => Value::test_string("Cargo.lock"),
218                        "type" => Value::test_string("toml")
219                    }),
220                ])),
221            },
222        ]
223    }
224}
225
226fn select(
227    engine_state: &EngineState,
228    call_span: Span,
229    columns: Vec<CellPath>,
230    input: PipelineData,
231) -> Result<PipelineData, ShellError> {
232    let mut unique_rows: BTreeSet<usize> = BTreeSet::new();
233
234    let mut new_columns = vec![];
235
236    for column in columns {
237        let CellPath { ref members } = column;
238        match members.first() {
239            Some(PathMember::Int { val, span, .. }) => {
240                if members.len() > 1 {
241                    return Err(ShellError::GenericError {
242                        error: "Select only allows row numbers for rows".into(),
243                        msg: "extra after row number".into(),
244                        span: Some(*span),
245                        help: None,
246                        inner: vec![],
247                    });
248                }
249                unique_rows.insert(*val);
250            }
251            _ => {
252                if !new_columns.contains(&column) {
253                    new_columns.push(column)
254                }
255            }
256        };
257    }
258    let columns = new_columns;
259
260    let input = if !unique_rows.is_empty() {
261        let metadata = input.metadata();
262        let pipeline_iter: PipelineIterator = input.into_iter();
263
264        NthIterator {
265            input: pipeline_iter,
266            rows: unique_rows.into_iter().peekable(),
267            current: 0,
268        }
269        .into_pipeline_data_with_metadata(
270            call_span,
271            engine_state.signals().clone(),
272            metadata,
273        )
274    } else {
275        input
276    };
277
278    #[cfg(feature = "sqlite")]
279    // Pushdown optimization: handle 'select' on SQLiteQueryBuilder for lazy column selection
280    if let PipelineData::Value(Value::Custom { val, .. }, ..) = &input
281        && let Some(table) = val.as_any().downcast_ref::<SQLiteQueryBuilder>()
282    {
283        // Check if all columns are simple string paths
284        let mut select_columns = Vec::new();
285        let mut all_simple = true;
286        for col in &columns {
287            if col.members.len() == 1 {
288                if let PathMember::String { val, .. } = &col.members[0] {
289                    select_columns.push(val.clone());
290                } else {
291                    all_simple = false;
292                    break;
293                }
294            } else {
295                all_simple = false;
296                break;
297            }
298        }
299        if all_simple {
300            let select_str = if select_columns.is_empty() {
301                "*".to_string()
302            } else {
303                select_columns.join(", ")
304            };
305            let new_table = table.clone().with_select(select_str);
306            return Ok(Value::custom(Box::new(new_table), call_span).into_pipeline_data());
307        }
308    }
309
310    match input {
311        PipelineData::Value(v, metadata, ..) => {
312            let span = v.span();
313            match v {
314                Value::List {
315                    vals: input_vals, ..
316                } => Ok(input_vals
317                    .into_iter()
318                    .map(move |input_val| {
319                        if !columns.is_empty() {
320                            let mut record = Record::new();
321                            for path in &columns {
322                                match input_val.follow_cell_path(&path.members) {
323                                    Ok(fetcher) => {
324                                        record.push(path.to_column_name(), fetcher.into_owned());
325                                    }
326                                    Err(e) => return Value::error(e, call_span),
327                                }
328                            }
329
330                            Value::record(record, span)
331                        } else {
332                            input_val.clone()
333                        }
334                    })
335                    .into_pipeline_data_with_metadata(
336                        call_span,
337                        engine_state.signals().clone(),
338                        metadata,
339                    )),
340                _ => {
341                    if !columns.is_empty() {
342                        let mut record = Record::new();
343
344                        for cell_path in columns {
345                            let result = v.follow_cell_path(&cell_path.members)?;
346                            record.push(cell_path.to_column_name(), result.into_owned());
347                        }
348
349                        Ok(Value::record(record, call_span)
350                            .into_pipeline_data_with_metadata(metadata))
351                    } else {
352                        Ok(v.into_pipeline_data_with_metadata(metadata))
353                    }
354                }
355            }
356        }
357        PipelineData::ListStream(stream, metadata, ..) => Ok(stream
358            .map(move |x| {
359                if !columns.is_empty() {
360                    let mut record = Record::new();
361                    for path in &columns {
362                        match x.follow_cell_path(&path.members) {
363                            Ok(value) => {
364                                record.push(path.to_column_name(), value.into_owned());
365                            }
366                            Err(e) => return Value::error(e, call_span),
367                        }
368                    }
369                    Value::record(record, call_span)
370                } else {
371                    x
372                }
373            })
374            .into_pipeline_data_with_metadata(call_span, engine_state.signals().clone(), metadata)),
375        _ => Ok(PipelineData::empty()),
376    }
377}
378
379struct NthIterator {
380    input: PipelineIterator,
381    rows: std::iter::Peekable<std::collections::btree_set::IntoIter<usize>>,
382    current: usize,
383}
384
385impl Iterator for NthIterator {
386    type Item = Value;
387
388    fn next(&mut self) -> Option<Self::Item> {
389        loop {
390            if let Some(row) = self.rows.peek() {
391                if self.current == *row {
392                    self.rows.next();
393                    self.current += 1;
394                    return self.input.next();
395                } else {
396                    self.current += 1;
397                    let _ = self.input.next()?;
398                    continue;
399                }
400            } else {
401                return None;
402            }
403        }
404    }
405}
406
407#[cfg(test)]
408mod tests {
409    use super::*;
410
411    #[test]
412    fn test_examples() {
413        use crate::test_examples;
414
415        test_examples(Select)
416    }
417}