Optional members in cell paths: Attempt 2 (#8379)

This is a follow up from https://github.com/nushell/nushell/pull/7540.
Please provide feedback if you have the time!

## Summary

This PR lets you use `?` to indicate that a member in a cell path is
optional and Nushell should return `null` if that member cannot be
accessed.

Unlike the previous PR, `?` is now a _postfix_ modifier for cell path
members. A cell path of `.foo?.bar` means that `foo` is optional and
`bar` is not.

`?` does _not_ suppress all errors; it is intended to help in situations
where data has "holes", i.e. the data types are correct but something is
missing. Type mismatches (like trying to do a string path access on a
date) will still fail.

### Record Examples

```bash

{ foo: 123 }.foo # returns 123

{ foo: 123 }.bar # errors
{ foo: 123 }.bar? # returns null

{ foo: 123 } | get bar # errors
{ foo: 123 } | get bar? # returns null

{ foo: 123 }.bar.baz # errors
{ foo: 123 }.bar?.baz # errors because `baz` is not present on the result from `bar?`
{ foo: 123 }.bar.baz? # errors
{ foo: 123 }.bar?.baz? # returns null
```

### List Examples
```
〉[{foo: 1} {foo: 2} {}].foo
Error: nu:🐚:column_not_found

  × Cannot find column
   ╭─[entry #30:1:1]
 1 │ [{foo: 1} {foo: 2} {}].foo
   ·                    ─┬  ─┬─
   ·                     │   ╰── cannot find column 'foo'
   ·                     ╰── value originates here
   ╰────
〉[{foo: 1} {foo: 2} {}].foo?
╭───┬───╮
│ 0 │ 1 │
│ 1 │ 2 │
│ 2 │   │
╰───┴───╯
〉[{foo: 1} {foo: 2} {}].foo?.2 | describe
nothing

〉[a b c].4? | describe
nothing

〉[{foo: 1} {foo: 2} {}] | where foo? == 1
╭───┬─────╮
│ # │ foo │
├───┼─────┤
│ 0 │   1 │
╰───┴─────╯
```

# Breaking changes

1. Column names with `?` in them now need to be quoted.
2. The `-i`/`--ignore-errors` flag has been removed from `get` and
`select`
1. After this PR, most `get` error handling can be done with `?` and/or
`try`/`catch`.
4. Cell path accesses like this no longer work without a `?`:
```bash
〉[{a:1 b:2} {a:3}].b.0
2
```
We had some clever code that was able to recognize that since we only
want row `0`, it's OK if other rows are missing column `b`. I removed
that because it's tricky to maintain, and now that query needs to be
written like:


```bash
〉[{a:1 b:2} {a:3}].b?.0
2
```

I think the regression is acceptable for now. I plan to do more work in
the future to enable streaming of cell path accesses, and when that
happens I'll be able to make `.b.0` work again.
This commit is contained in:
Reilly Wood
2023-03-15 20:50:58 -07:00
committed by GitHub
parent d3be5ec750
commit 21b84a6d65
32 changed files with 510 additions and 277 deletions

View File

@ -240,7 +240,11 @@ mod test {
},
Value::CellPath {
val: CellPath {
members: vec![PathMember::Int { val: 0, span }],
members: vec![PathMember::Int {
val: 0,
span,
optional: false,
}],
},
span,
},

View File

@ -208,10 +208,11 @@ mod util {
let path = PathMember::String {
val: header.to_owned(),
span: Span::unknown(),
optional: false,
};
item.clone()
.follow_cell_path(&[path], false, false)
.follow_cell_path(&[path], false)
.unwrap_or_else(|_| item.clone())
}
item => item.clone(),

View File

@ -109,10 +109,7 @@ fn dropcol(
let mut vals = vec![];
for path in &keep_columns {
let fetcher =
input_val
.clone()
.follow_cell_path(&path.members, false, false)?;
let fetcher = input_val.clone().follow_cell_path(&path.members, false)?;
cols.push(path.into_string());
vals.push(fetcher);
}
@ -136,10 +133,7 @@ fn dropcol(
let mut vals = vec![];
for path in &keep_columns {
let fetcher =
input_val
.clone()
.follow_cell_path(&path.members, false, false)?;
let fetcher = input_val.clone().follow_cell_path(&path.members, false)?;
cols.push(path.into_string());
vals.push(fetcher);
}
@ -155,9 +149,7 @@ fn dropcol(
let mut vals = vec![];
for cell_path in &keep_columns {
let result = v
.clone()
.follow_cell_path(&cell_path.members, false, false)?;
let result = v.clone().follow_cell_path(&cell_path.members, false)?;
cols.push(cell_path.into_string());
vals.push(result);

View File

@ -74,7 +74,7 @@ fn empty(
for val in input {
for column in &columns {
let val = val.clone();
match val.follow_cell_path(&column.members, false, false) {
match val.follow_cell_path(&column.members, false) {
Ok(Value::Nothing { .. }) => {}
Ok(_) => return Ok(Value::boolean(false, head).into_pipeline_data()),
Err(err) => return Err(err),

View File

@ -279,7 +279,7 @@ fn flat_value(columns: &[CellPath], item: &Value, _name_tag: Span, all: bool) ->
if !columns.is_empty() {
let cell_path =
column_requested.and_then(|x| match x.members.first() {
Some(PathMember::String { val, span: _ }) => Some(val),
Some(PathMember::String { val, span: _, .. }) => Some(val),
_ => None,
});

View File

@ -41,11 +41,6 @@ If multiple cell paths are given, this will produce a list of values."#
"the cell path to the data",
)
.rest("rest", SyntaxShape::CellPath, "additional cell paths")
.switch(
"ignore-errors",
"when there are empty cells, instead of erroring out, replace them with nothing",
Some('i'),
)
.switch(
"sensitive",
"get path in a case sensitive manner",
@ -65,13 +60,12 @@ If multiple cell paths are given, this will produce a list of values."#
let cell_path: CellPath = call.req(engine_state, stack, 0)?;
let rest: Vec<CellPath> = call.rest(engine_state, stack, 1)?;
let sensitive = call.has_flag("sensitive");
let ignore_errors = call.has_flag("ignore-errors");
let ctrlc = engine_state.ctrlc.clone();
let metadata = input.metadata();
if rest.is_empty() {
input
.follow_cell_path(&cell_path.members, call.head, !sensitive, ignore_errors)
.follow_cell_path(&cell_path.members, call.head, !sensitive)
.map(|x| x.into_pipeline_data())
} else {
let mut output = vec![];
@ -81,9 +75,7 @@ If multiple cell paths are given, this will produce a list of values."#
let input = input.into_value(span);
for path in paths {
let val = input
.clone()
.follow_cell_path(&path.members, !sensitive, false);
let val = input.clone().follow_cell_path(&path.members, !sensitive);
output.push(val?);
}

View File

@ -22,11 +22,6 @@ impl Command for Select {
(Type::Record(vec![]), Type::Record(vec![])),
(Type::Table(vec![]), Type::Table(vec![])),
])
.switch(
"ignore-errors",
"when an error occurs, instead of erroring out, suppress the error message",
Some('i'),
)
.rest(
"rest",
SyntaxShape::CellPath,
@ -58,9 +53,8 @@ produce a table, a list will produce a list, and a record will produce a record.
) -> Result<PipelineData, ShellError> {
let columns: Vec<CellPath> = call.rest(engine_state, stack, 0)?;
let span = call.head;
let ignore_errors = call.has_flag("ignore-errors");
select(engine_state, span, columns, input, ignore_errors)
select(engine_state, span, columns, input)
}
fn examples(&self) -> Vec<Example> {
@ -97,7 +91,6 @@ fn select(
call_span: Span,
columns: Vec<CellPath>,
input: PipelineData,
ignore_errors: bool,
) -> Result<PipelineData, ShellError> {
let mut unique_rows: HashSet<usize> = HashSet::new();
@ -106,11 +99,8 @@ fn select(
for column in columns {
let CellPath { ref members } = column;
match members.get(0) {
Some(PathMember::Int { val, span }) => {
Some(PathMember::Int { val, span, .. }) => {
if members.len() > 1 {
if ignore_errors {
return Ok(Value::nothing(call_span).into_pipeline_data());
}
return Err(ShellError::GenericError(
"Select only allows row numbers for rows".into(),
"extra after row number".into(),
@ -172,11 +162,7 @@ fn select(
let mut vals = vec![];
for path in &columns {
//FIXME: improve implementation to not clone
match input_val.clone().follow_cell_path(
&path.members,
false,
ignore_errors,
) {
match input_val.clone().follow_cell_path(&path.members, false) {
Ok(fetcher) => {
allempty = false;
cols.push(path.into_string().replace('.', "_"));
@ -214,10 +200,7 @@ fn select(
let mut vals = vec![];
for path in &columns {
//FIXME: improve implementation to not clone
match x
.clone()
.follow_cell_path(&path.members, false, ignore_errors)
{
match x.clone().follow_cell_path(&path.members, false) {
Ok(value) => {
cols.push(path.into_string().replace('.', "_"));
vals.push(value);
@ -246,10 +229,7 @@ fn select(
for cell_path in columns {
// FIXME: remove clone
match v
.clone()
.follow_cell_path(&cell_path.members, false, ignore_errors)
{
match v.clone().follow_cell_path(&cell_path.members, false) {
Ok(result) => {
cols.push(cell_path.into_string().replace('.', "_"));
vals.push(result);

View File

@ -143,7 +143,7 @@ fn update(
ctrlc,
)
} else {
if let Some(PathMember::Int { val, span }) = cell_path.members.get(0) {
if let Some(PathMember::Int { val, span, .. }) = cell_path.members.get(0) {
let mut input = input.into_iter();
let mut pre_elems = vec![];

View File

@ -165,7 +165,7 @@ fn upsert(
ctrlc,
)
} else {
if let Some(PathMember::Int { val, span }) = cell_path.members.get(0) {
if let Some(PathMember::Int { val, span, .. }) = cell_path.members.get(0) {
let mut input = input.into_iter();
let mut pre_elems = vec![];

View File

@ -279,12 +279,10 @@ fn format_record(
.map(|path| PathMember::String {
val: path.to_string(),
span: *span,
optional: false,
})
.collect();
match data_as_value
.clone()
.follow_cell_path(&path_members, false, false)
{
match data_as_value.clone().follow_cell_path(&path_members, false) {
Ok(value_at_column) => {
output.push_str(value_at_column.into_string(", ", config).as_str())
}

View File

@ -277,9 +277,9 @@ fn convert_to_list(
&[PathMember::String {
val: header.into(),
span: head,
optional: false,
}],
false,
false,
),
_ => Ok(item.clone()),
};

View File

@ -934,8 +934,9 @@ fn convert_to_table(
let path = PathMember::String {
val: text.clone(),
span: head,
optional: false,
};
let val = item.clone().follow_cell_path(&[path], false, false);
let val = item.clone().follow_cell_path(&[path], false);
match val {
Ok(val) => DeferredStyleComputation::Value { value: val },
@ -1321,8 +1322,12 @@ fn create_table2_entry(
match item {
Value::Record { .. } => {
let val = header.to_owned();
let path = PathMember::String { val, span: head };
let val = item.clone().follow_cell_path(&[path], false, false);
let path = PathMember::String {
val,
span: head,
optional: false,
};
let val = item.clone().follow_cell_path(&[path], false);
match val {
Ok(val) => convert_to_table2_entry(